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:
- pass arguments to emplace in a tuple
- read the tuple from my emplace function
- unpack the tuple
- 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.
emplaceA(...)andemplaceB(...)methods ?