6

I'm building a container which uses, as its internal representation, a couple of standard containers, each with a different type. I need to code my version of emplace(..) and here's where I got stuck.

This is what my code boils down to, with an emplace function that will not compile but hopefully will make you understand what I'm trying to achieve:

template <typename A, typename B>
class MyContainer
{
    public:
        template <typename... ArgsA, typename... ArgsB>
        void emplace(ArgsA&&... argsA, ArgsB&&... argsB)
        {
            // do custom logic
            a_container.emplace_back(std::forward<ArgsA>(argsA)...);
            b_container.emplace_back(std::forward<ArgsB>(argsB)...);
        }

    private:
        std::vector<A> a_container;
        std::vector<B> b_container;
};

As I said, this obviously doesn't work as the compiler can't magically decide where to stop reading the first argument pack and start the second one. When I see this kind of problems, my experience with meta-programming makes me immediately reach out for std::tuple. If there's a better solution, please chime in, but from this point onwards I'll assume this is the simplest way to move forward.

So, using std::tuple, the problem can be decomposed in a few different steps. In each step, I need to ensure the nature (r-value vs l-value) of the arguments is preserved:

  1. pass arguments to emplace in a tuple
  2. read the tuple from my emplace function
  3. unpack the tuple
  4. pass the unpacked tuple elements to emplace_back(..)

Regarding point 1., while the caller may already have an std::tuple on hand, most of the times it will probably call emplace using directly the parameters that it wants to ultimately be forwarded to the two constructors. This seems exactly what std::forward_as_tuple was created for, if I'm not mistaken. If so, I would consider it done.

Point 2. presents a little incovenience. That is, rewriting emplace to accept std::tuple:

template <typename... ArgsA, typename... ArgsB>
void emplace(std::tuple<ArgsA...> tupleA, std::tuple<ArgsB...> tupleB)

Unfortunately those variadic ArgsA and ArgsB can't be forwarding references as they are not deduced directly by the nature of tupleA and tupleB, rather from their types. So I'll have to remember that those may be both r-value and l-value reference and clean that up when I'll forward them.

Moreover, I think the way we pass around std::tuple can introduce a weird behaviour. Above I wrote a "lazy version" that takes std::tuple by copy, but most of the times when I write a container I prefer, as the standard does too, to use both a const std::tuple<..>& and a std::tuple<..>&& version for maximum flexibility. In this case, the const std::tuple<..>& version will have std::get extract only l-values version of our arguments due to reference-collapsing rules, which defeats the point. So I'm thinking about writing only std::tuple<..>&& and (maybe) mantaining the copy version.

Points 3. and 4. are the most difficult. I reckon I'll have to solve them together and I'm quite surprised the standard, as far as I understand, offers std::forward_as_tuple but not the inverse operation. There's a subtle problem here that prevents the simple solution of forwarding using code like this:

template <typename... ArgsA, typename... ArgsB>
void emplace(std::tuple<ArgsA...> tupleA, std::tuple<ArgsB...> tupleB)
{
    // do custom logic
    a_container.emplace_back(std::forward<std::remove_reference_t<ArgsA>>(std::get<ArgsA>(tupleA))...);
    ...

This completely breaks down when you have repeated types in the std::tuple. So we have to use index access and my best idea so far is to write an std::invoke wrapper that will use an std::integer_sequence to unpack the std::tuple, but that's just an intuition and I need to work out the details. As said and shown above, in all this mayhem, I'll still have to remember to remove references from the types the std::tuple is made of in order to perfect-forward the respective arguments.

I am especially seeking simpler solutions to points 3. and 4.

3
  • Why not have separate emplaceA(...) and emplaceB(...) methods ? Commented Oct 26 at 9:16
  • @wohlstad that is not possible, given the two containers are an internal representation. and the client should know nothing about that. Moreover, while maybe the starting code may suggest those types are independent, they are not and - incidentally - I used std::vector to simplify the example. And no, using std::pair would not solve the issue. Anyway, I'm interested in how to solve this problem to use the solution in other contexts if the opportunity presents itself. Commented Oct 26 at 9:25
  • Fair enough. The accepted answer is then indeed the way to go. Commented Oct 26 at 10:33

2 Answers 2

11

std::forward_as_tuple is indeed a way to go. Other tuple creating methods (std::tie/std::make_tuple) might be enough too depending of situation.

std::apply allows to do the unpacking:

template <typename A, typename B>
class MyContainer
{
public:
    template <typename... ArgsA, typename... ArgsB>
    void emplace(std::tuple<ArgsA...> argsA, std::tuple<ArgsB...> argsB)
    {
        // do custom logic
        std::apply([this](auto&&... args){
                a_container.emplace_back(std::forward<ArgsA>(args)...);
            }, argsA);
        std::apply([this](auto&&... args){
                b_container.emplace_back(std::forward<ArgsB>(args)...);
            }, argsB);
    }

private:
    std::vector<A> a_container;
    std::vector<B> b_container;
};

Demo

or even simpler:

template <typename A, typename B>
class MyContainer
{
public:
    template <typename TupleA, typename TupleB>
    void emplace(TupleA&& tupleA, TupleB&& tupleB)
    {
        // do custom logic
        std::apply([this](auto&&... args){
                a_container.emplace_back(std::forward<decltype(args)>(args)...);
            }, std::forward<TupleA>(tupleA));
        std::apply([this](auto&&... args){
                b_container.emplace_back(std::forward<decltype(args)>(args)...);
            }, std::forward<TupleB>(tupleB));
    }

private:
    std::vector<A> a_container;
    std::vector<B> b_container;
};

Demo

Sign up to request clarification or add additional context in comments.

4 Comments

First of all, thanks for suggesting std::apply; I wasn't aware of its existance and it does exactly what I was trying to recreate, solving by itself points 3 and 4. However, I have a few observations/open points. In your first example: 1) you should std::move argsA and argsB; 2) I'm looking at std::forward right now and, while I think you can forward on ArgsA and ArgsB directly, I think it may be clearer to std::remove_reference before using them as the type argument for std::forward.
Also, I'm still very doubtful about using const std::tuple& as argument for emplace, which your second solution implies. If my analysis on std::get is right, it would probably create a situation where a caller uses an already created tuple in the wrong way and it doesn't actually moves the arguments
Trick with tuple is that you might have reference (and cv-qualifiers) for inner type, i.e. std::tuple<const Obj&, Obj&&, Obj> and the tuple itself might use reverence (std::tuple<Ts..>&, std::tuple<Ts...>&&, std::tuple<Ts...>), so many possibilities to pass non-optimal tuple. Case 2 is the more correct in that regard. In case 1, you should probably check there are only (l or r-value) reference in the tuple.
Have a look at constructor of std::pair (9) taking a std::piecewise_construct_t for model.
1

As a complementary observation, it would be possible to use some conventional packsep object (instance of Separator class) acting as a pack separator, e.g.

mycontainer.emplace (
   "a",  packsep, 
   "b",c
);

where two sub packs would be identified and then used to initialize both containers.

More generally, one could define some subpacks function taking variadic arguments, potentially separated by some packsep object (or any instance of Separator class). This function would call a provided functor with sub packs built from the incoming arguments. For instance:

auto main() -> int {

    char c='c';
    double x=3.14;
    
    subpacks ([] <size_t I> (auto&&...args) {  std::cout << "[" << I << "] "; ((std::cout << args << " "),...) << "\n"; },
        "a",     Separator{}, 
        "b",c,   Separator{}, 
        x,       Separator{},
        1,2,3
    );
}

would produce

[0] a 
[1] b c 
[2] 3.14 
[3] 1 2 3 

To achieve this, one should first define some Separator class that can be instanciated for providing some pack separator object:

struct Separator {} packsep;

Designing the subpacks function would imply several meta-programming steps:

  1. computing an array holding the indexes where instances of Separator appear in the incoming arguments
  2. building an array holding the starting index of each identified pack
  3. calling the provided functor with each sub pack

The whole code for subpacks would be

template <typename Fct, typename...Args>
void subpacks (Fct fct, Args&&...args)
{
    auto t = std::forward_as_tuple(std::forward<Args>(args)...);

    // 1. We build an array holding indexes of objects from Separator 
    static constexpr auto delimArray = [&] {
        // we first count how many times we got a Delim object in the arguments.
        constexpr std::size_t N = (( std::is_same_v<std::decay_t<decltype(args)>,Separator> ? 1:0) + ... );
        // then we memorize indexes where such an object appears in the arguments
        std::array<std::size_t,N> res;
        std::size_t i=0,j=0;
        ([&](auto&& x) {
            if (std::is_same_v<std::decay_t<decltype(x)>,Separator>)  {  res[j++]=i; }
            i++;
         }(args), ...);
        return res;
    } ();
    
    // 2. We build an array holding the starting index of each identified pack
    static constexpr auto indexes = [&] {
        std::array<std::size_t,delimArray.size()+2> res;
        res[0]=0;
        for (std::size_t i=0; i<delimArray.size(); i++)  { res[i+1] = 1+delimArray[i]; }
        res[res.size()-1] = 1 + sizeof...(args);
        return res;
    }();
    
    // We call the provided functor with one specific sub pack
    auto inner = [&] <std::size_t K> () {
        constexpr auto N = indexes[K+1]-indexes[K+0]-1;
        return [&]<std::size_t... Is>(std::index_sequence<Is...>)  {  
            fct.template operator() <K> (std::get<indexes[K]+Is>(std::forward<decltype(t)>(t))...);  
        }(std::make_index_sequence<N>{});
    };

    // 3. We call the provided functor several times, each time with one sub pack
    [&]<std::size_t... Ks>(std::index_sequence<Ks...>)  {
            (inner.template operator()<Ks>(), ...);
    } (std::make_index_sequence<indexes.size()-1>{});
}

Demo

Now, if we use subpacks in your context, one would have

template <typename...Ts>
class MyContainer
{
public:
    template <typename...Args>
    void emplace(Args&&...args)
    {
        subpacks ([this]<std::size_t I> (auto&&...aargs) {
             std::get<I>(containers).emplace_back(std::forward<decltype(aargs)>(aargs)...);
        }, std::forward<Args>(args)...);
    }

private:
    using containers_t  = decltype(std::make_tuple (std::vector<Ts>{}...));
    containers_t containers;
};

auto main() -> int {
    MyContainer<Verbose,Verbose,Verbose,Verbose,Verbose> c;

    c.emplace (
        1,   Separator{}, 
             Separator{}, 
        2,3, Separator{},
        4,   Separator{},
        5,6
    );
}

Demo

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.