Brian Rodriguez
Brian Rodriguez

Reputation: 4359

Use std::tuple member references during construction?

#include "MassivePOD.h"
#include "DependantClass.h" // Constructor: `DependantClass(MassivePOD&)`

class ExplicitSolution
{
public:
    ExplicitSolution() : m_pod{...}, m_dep{m_pod} { };
private:
    MassivePOD  m_pod;
    DependantClass  m_dep;
};

template<typename... T>
class TemplatedSolution
{
public:
    template<typename... TupArgs> TemplatedSolution(TupArgs...);
private:
    // Can assume in-order construction works
    std::tuple<T...>  m_tuple;
};


int main()
{
    ExplicitSolution a{}; // Easy!
    TemplatedSolution<MassivePOD, DependantClass> b{
        std::forward_as_tuple(...) // Forwarded to MassivePOD constructor
      , std::forward_as_tuple(???) // Forwarded to DependantClass constructor
        }; // Hard?
}

I hope this example illustrates my problem. I'd like to make a reference to an earlier constructed std::tuple member before the entire std::tuple has been constructed. Is there an elegant solution to this? I know it's possible using void * hackery, but I'd rather seek some help before going down that dark lonely road.

Idea 1

I've tried making a get_ref function but I get the same problem that I can't access the member function of an object that hasn't been created yet. Here's one of my attempts anyway, though.

#include <tuple>
#include <utility>

class Foo // simplified base-case
{
public:
    constexpr Foo(int x, char y, int& z) : m_tup{x, y, z*5.0} { };

    constexpr int& get_int() { return std::get<0>(m_tup); };
    constexpr char& get_char() { return std::get<1>(m_tup); };
    constexpr double& get_double() { return std::get<2>(m_tup); };

private:
    std::tuple<int, char, double>  m_tup;
};

int main()
{
    auto super = Foo(5, 'a', ::get_int()); // ???
}

Demo

Idea 2

Then I figured maybe I could do something like what std::function does with std::place_holders, so a static object that contains a pointer/reference to where each element of the std::tuple would be. I think this is worth trying but I have no clue how to implement it in practice...

Upvotes: 2

Views: 154

Answers (2)

Claudiu
Claudiu

Reputation: 229361

Another proposal: the tuple class, rather than taking parameters for the constructors of the members, instead takes functions which receive the being-constructed instance and which return the members. These functions can then refer to the previously-constructed members via getters. All you have to do is make sure that the getters for earlier members work and don't invoke undefined behavior if called while the later members are being constructed.

Here's an example hard-coded for two elements to demonstrate the principle. I will leave it to you to get it to work with n elements. Note that essentially you would be re-implementing a tuple here - I'm not sure whether you can do this with the existing std::tuple.

template<typename A, typename B>
class TemplatedSolution
{
public:
    template<typename AF, typename BF> TemplatedSolution(AF af, BF bf)
        : a(af(*this))
        , b(bf(*this))
    {
    }

    A& get_first() {
        return a;
    }

    B& get_second() {
        return b;
    }

private:
    A a;
    B b;
};

Usage:

typedef TemplatedSolution<MassivePOD, DependantClass> ClassT;

ClassT b{
    [](ClassT& res) { return MassivePOD(); },
    [](ClassT& res) { return DependantClass(res.get_first()); },
};

Because of return-value optimization, the MassivePODs will get constructed right in the TemplatedSolution instance.

A fully-working example which demonstrates no copies is available here.

Upvotes: 1

Claudiu
Claudiu

Reputation: 229361

What about using std::shared_ptr and storing it as a std::shared_ptr in TupleWrapper instead?

auto p_first_member_tmp = std::make_shared<MassivePOD>(
    std::forward_as_tuple(...));

TupleWrapper<std::shared_ptr<MassivePOD>, DependantClass> wrapper{
    p_first_member_tmp, 
    std::forward_as_tuple(*p_first_member_tmp, ...)}

It is an extra level of indirection, but your expensive-to-copy-or-move structure won't be copied or moved at least.

I was considering why this is never an issue in a language like Python, and it's because the object only gets created once, then everything else just points to it - which is approximated by this approach here. You also won't have to keep any additional track of the lifetime of the object since shared_ptr does that for you already.

Upvotes: 0

Related Questions