3

Is there any way to ensure members of a non-aggregate type are initialized with no copy and no move, i.e. using in-place construction? Suppose I have a heavy type

#include <iostream>
#include <string>
#include <utility>

struct S {
    std::string x;

    explicit S(std::string x) : x{x} { std::cout << "1-arg ctor\n"; }

    S(const S &other) : x{other.x} { std::cout << "copy ctor\n"; }

    S(S &&other) noexcept : x{std::move(other.x)} {
        std::cout << "move ctor\n";
    }

    ~S() noexcept { std::cout << "dtor\n"; }
};

Note: The std::string is just for example purposes, I know string_view could be used instead of taking the string by value.

Apparently, I get a different sequence of copies/moves if I try to construct an S as a member of another struct depending on whether that stuct is an aggregate type:

struct Agg {
    S s;
};

struct NotAgg {
    S s;

    explicit NotAgg(S s) : s{s} {};
};

struct NotAggMove {
    S s;

    explicit NotAggMove(S &&s) : s{std::move(s)} {};
};

int main() {
    std::cout << "=== AGG ===\n";
    { Agg agg{S("hello world this is a long string")}; }
    std::cout << "=== NOT AGG ===\n";
    { NotAgg not_agg{S("hello world this is a long string")}; }
    std::cout << "=== NOT AGG MOVE ===\n";
    { NotAggMove not_agg_move{S("hello world this is a long string")}; }
    std::cout << "=== DONE ===\n";

    return 0;
}

The above code (live demo: https://godbolt.org/z/o5shr8nME) produces the following output in all of (GCC, Clang, MSVC) with optimizations turned on:

=== AGG ===
1-arg ctor
dtor
=== NOT AGG ===
1-arg ctor
copy ctor
dtor
dtor
=== NOT AGG MOVE ===
1-arg ctor
move ctor
dtor
dtor
=== DONE ===

As we can see, in the aggregate type, the S is constructed directly into the aggregate with no copies and no moves, but the non-aggregates both have a spurious copy or move. In the case of the move, I guess I can understand it as analogous to a pessimizing move preventing RVO, like returning std::move(local_variable) where normally the compiler would use RVO, but since I explicitly moved it gives me a move. However, I don't see any reason why the compiler can't elide the copy in the second case.

The argument to NotAgg(S s) is a prvalue so the copy into the argument s can be elided, then s is used to initialize the member s, which is quite similar in my mind to returning a local variable.

Actual question: Is it possible to design my non-aggregate class NotAgg and/or call the constructor or otherwise create the struct in such a way that the compiler is allowed to construct the member s in-place, i.e. with no copy and no move? Or is this only possible for aggregates?

mCoding
  • 4,059
  • 1
  • 5
  • 11
  • I strongly suspect this can be done only for aggregates. Btw, your class can be improved: you should move `x` in the constructor. – bolov Oct 07 '22 at 01:20
  • @bolov Good catch! That's actually an instance of the very thing I'm confused about in this question. A copy happens there (`x{x}`) but it seems like copy elision should be allowed (even though evidently it is not happening). – mCoding Oct 07 '22 at 01:50
  • copy-elision is only allowed in some specific cases. Last usage is not one of them. – Jarod42 Oct 07 '22 at 09:16

0 Answers0