1

I'd like to initialize two non-static private template member variables of a class template from a temporary in-place, i.e., without making a copy or move.

For clarification, consider the following example code:

#include <iostream>

struct P {
    P(int n) : n_ { n } {};

    P(P&&) { std::cout << "P:moved" << std::endl; }
    P(const P&) { std::cout << "P:copied" << std::endl; }

    int n_;
};

struct Q {
    Q(double x) : x_ { x } {};

    Q(Q&&) { std::cout << "Q:moved" << std::endl; }
    Q(const Q&) { std::cout << "Q:copied" << std::endl; }

    double x_;
};

/* note that P and Q are just two illustrative examples;
   don't count on anything specific in them; with respect
   to the asked question, they should just represent two
   arbitrary classes with arbitrary ctors */

template<typename U, typename V>
class X {
    public:
        X(U u, V v) : u_ { u }, v_ { v } {}

    private:
        U u_;
        V v_;
};

int
main(
) {
    X x { P { 0 }, Q { 0.0 } };

    return 0;
}

Output (with gcc 8.2.0) is P:copied Q:copied because u and v are copied to u_ and v_ in X's ctor, respectively. However, since the temporaries P { 0 } and Q { 0.0 } are only used to initialize u_ and v_, respectively, I wonder whether one can initialize both member variables in-place. I'd like to see neither copied nor moved here. Even more, I'd like to run this code with copy and move ctor of P and Q deleted.

Is this possible in C++17 (or earlier), and if so, how?

plexando
  • 1,151
  • 6
  • 22
  • 1
    "How can I initialize ... without making a copy or move?" - How would you expect to initialize *anything* if not by direct initialization *or* a *copy* or *move*?? – Jesper Juhl Aug 05 '19 at 21:13
  • 3
    If you were to add constructors to `P` and `Q` that took `int`s, rather than objects, and one for `X` that took two `int`s, then you could use those `int`s to do direct-initialize the members `u_` and `v_`. – Marshall Clow Aug 05 '19 at 21:15
  • @JesperJuhl Aggregate initialization, copy elision, ... But unfortunately, not applicable here. – plexando Aug 05 '19 at 21:23

2 Answers2

3

Basically to do what you want, you need to build a kind of interface that std::pair uses to forward the arguments of the constructor of the members to the members. The way they do that is to build a tuple of the arguments and then delegate those tuples to another constructor that also gets std::integer_sequence's of the size of each tuple parameter pack so it can unpack the tuple uses those sequences to directly call the members constructor. The following code isn't perfect, but it will start you on the path to build a production version.

template<typename U, typename V>
class X {
    public:
        // old constructor that makes copies
        X(U u, V v) : u_ { u }, v_ { v } { std::cout << "X(U, V)\n"; }

        // this is the constructor the user code will call
        template<typename... Args1, typename... Args2>
        X(std::piecewise_construct_t pc, std::tuple<Args1...>&& u, std::tuple<Args2...>&& v) : 
            X(pc, std::move(u), std::move(v), std::make_index_sequence<sizeof...(Args1)>{}, std::make_index_sequence<sizeof...(Args2)>{}) {}

        // this is where the magic happens  Now that we have Seq1 and Seq2 we can
        // unpack the tuples into the constructor
        template<typename... Args1, typename... Args2, auto... Seq1, auto... Seq2>
        X(std::piecewise_construct_t pc, std::tuple<Args1...>&& u, std::tuple<Args2...>&& v, std::integer_sequence<size_t, Seq1...>, std::integer_sequence<size_t, Seq2...>) : 
            u_ { std::get<Seq1>(u)... }, v_ { std::get<Seq2>(v)... } {}

    private:
        U u_;
        V v_;
};

int main() 
{
    // and now we build an `X` by saying we want the tuple overload and building the tuples
    X<P,Q> x { std::piecewise_construct, std::forward_as_tuple(0), std::forward_as_tuple(0.0) };
    // Unfortunetly we don't get CTAD with this.  Not sure if that can be fixed with a deduction guide
}

You could also look at one of the opensource C++ libraries like libc++ or libstdc++ to see how they implement std::pair's piecewise constructor to get a handle on how to make it production worthy.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • I also thought about passing constructor arguments of P and Q as tuples into X in order to "emplace" P and Q in X's constructor. But I hoped someone could come up with a less cumbersome solution. Lack of CTAD makes it even more clumsy. Nevertheless your answer solves the problem. Thanks. – plexando Aug 06 '19 at 11:59
0

As plexando already pointed out in the comments, we can't get that far as neither moving nor copying. One copying is elided by not constructing any object P or Q in the body of main (at the call site), but parameters u and v in X::X(U u, V v) have to be valid, from which the value is either moved of copied. This cannot be elided.

The best we can do is make X::X use universal references and then forward, which will cause move instead a copy to be performed.

X(U&& u, V&& v) : u_{ std::forward<U&&>(u) }, v_{ std::forward<V&&>(v) } {}

This prints moved for me twice. There is however another option similar to how arguments in standard_container::emplace are forwarded. Using some disgusting std::enable_if sorcery, you can write this constructor

template<typename TO_U, typename TO_V, typename = std::enable_if_t<std::is_constructible_v<U, TO_U> && std::is_constructible_v<V, TO_V>>> X(TO_U&& to_u, TO_V&& to_v) : u_(std::forward<TO_U&&>(to_u)), v_(std::forward<TO_V&&>(to_v)) {}

which postpones any construction to the latest moment. In this case there is no moved or copied printed and it's SFINAEd away if data members u_ and v_ can't be constructed from passed arguments. It is however ut to you, whether it's applicable to your problem or your classes are too complex to construct them this way.

TLDR: If you can't postpone construction by perfectly formarding arguments as long as possible, you will always copy or move, because copy elision does not reach this far.