18

I have several classes for which I wish to check whether a default move constructor is being generated. Is there a way to check this (be it a compile-time assertion, or parsing the generated object files, or something else)?


Motivational example:

class MyStruct : public ComplicatedBaseClass {
    std::vector<std::string> foo; // possibly huge
    ComplicatedSubObject bar;
};

If any member of any base or member of either Complicated...Object classes cannot be moved, MyStruct will not have its implicit move constructor generated, and may thus fail to optimize away the work of copying foo, when a move could be done, even though foo is movable.


I wish to avoid:

  1. tediously checking the conditions for implicit move ctor generation,
  2. explicitly and recursively defaulting the special member functions of all affected classes, their bases, and their members—just to make sure a move constructor is available.

I have already tried the following and they do not work:

  1. use std::move explicitly—this will invoke the copy constructor if no move constructor is available.
  2. use std::is_move_constructible—this will succeed when there is a copy constructor accepting const Type&, which is generated by default (as long as the move constructor is not explicitly deleted, at least).
  3. use nm -C to check the presence of move constructor (see below). However, an alternative approach is viable (see answer).

I tried looking at the generated symbols of a trivial class like this:

#include <utility>

struct MyStruct {
    MyStruct(int x) : x(x) {}
    //MyStruct(const MyStruct& rhs) : x(rhs.x) {}
    //MyStruct(MyStruct&& rhs) : x(rhs.x) {}
    int x;
};

int main() {
    MyStruct s1(4);
    MyStruct s2(s1);
    MyStruct s3(std::move(s1));
    return s1.x + s2.x + s3.x; // Make sure nothing is optimized away
}

The generated symbols looks like this:

$ CXXFLAGS="-std=gnu++11 -O0" make -B x; ./x; echo $?; nm -C x | grep MyStruct | cut -d' ' -f3,4,5
g++ -std=gnu++11 -O0    x.cc   -o x
12
.pdata$_ZN8MyStructC1Ei
.pdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.text$_ZN8MyStructC1Ei
.text$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.xdata$_ZN8MyStructC1Ei
.xdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
MyStruct::MyStruct(int)
std::remove_reference<MyStruct&>::type&&

The output is the same when I explicitly default the copy and move constructors (no symbols).

With my own copy and move constructors, the output looks like this:

$ vim x.cc; CXXFLAGS="-std=gnu++11 -O0" make -B x; ./x; echo $?; nm -C x | grep MyStruct | cut -d' ' -f3,4,5
g++ -std=gnu++11 -O0    x.cc   -o x
12
.pdata$_ZN8MyStructC1Ei
.pdata$_ZN8MyStructC1EOKS_
.pdata$_ZN8MyStructC1ERKS_
.pdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.text$_ZN8MyStructC1Ei
.text$_ZN8MyStructC1EOKS_
.text$_ZN8MyStructC1ERKS_
.text$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
.xdata$_ZN8MyStructC1Ei
.xdata$_ZN8MyStructC1EOKS_
.xdata$_ZN8MyStructC1ERKS_
.xdata$_ZSt4moveIR8MyStructEONSt16remove_referenceIT_E4typeEOS3_
MyStruct::MyStruct(int)
MyStruct::MyStruct(MyStruct&&)
MyStruct::MyStruct(MyStruct const&)
std::remove_reference<MyStruct&>::type&& std::move<MyStruct&>(MyStruct&)

So it appears this approach also doesn't work.


However if the target class has a member with explicit move constructor, the implicitly generated move constructor will be visible for the target class. I.e. with this code:

#include <utility>

struct Foobar {
    Foobar() = default;
    Foobar(const Foobar&) = default;
    Foobar(Foobar&&) {}
};

struct MyStruct {
    MyStruct(int x) : x(x) {}
    int x;
    Foobar f;
};
int main() {
    MyStruct s1(4);
    MyStruct s2(s1);
    MyStruct s3(std::move(s1));
    return s1.x + s2.x + s3.x; // Make sure nothing is optimized away
}

I will get the symbol for MyStruct's move constructor, but not the copy constructor, as it appears to be fully implicit. I presume the compiler generates a trivial inlined move constructor if it can, and a non-trivial one if it must call other non-trivial move constructors. This still doesn't help me with my quest though.

Peter Mortensen
  • 30,738
  • 21
  • 105
  • 131
Irfy
  • 9,323
  • 1
  • 45
  • 67
  • So in the above code, all uses of the copy ctor are elided; its existence is checked, but it is not called. (elision is a technical term within the C++ standard if you don't recognize it). The move ctor, in comparison, is actually invoked. To force a call of the copy ctor, write `template T const& copy(T const& t){return t;}`, then `MyStruct s2(copy(s1));`. Then the copy ctor may show up in your dump? – Yakk - Adam Nevraumont Nov 26 '15 at 14:36
  • The important thing often isn't having a move ctor, but rather that the move operation is no-throw. A structure containing an array of 1000 raw bytes cannot be efficiently moved; there is little point in defining the move ctor. The copy ctor does the job just as well. Structures that require allocation often benefit from moving (as you can tear the allocation out); in that case, the copy ctor can throw (allocation failure), while the move ctor does not (as it just steals the data from the passed in object). Maybe that approach? – Yakk - Adam Nevraumont Nov 26 '15 at 14:39
  • @Yakk: I challenge you to prove that the copy constructor is elided :-) The fundamental precondition for any elision is that the source and target object *can* be treated as one and the same. Like returning a local object or a temporary from a function -- nothing is lost if we construct that object in the target object in the first place. Here, all objects are separate and elision cannot be performed. – Irfy Nov 26 '15 at 21:02
  • OTOH, your second comment led me to use an std::string which has explicit copy and move constructors. Indeed, the implicitly generated move constructor showed up for `MyStruct`. I'm guessing that this is to do with the constructor being trivial or not. Note that any optimization level leads to no constructor symbols at all -- presumably due to inlining. Wait, `-fno-inline`? – Irfy Nov 26 '15 at 21:05
  • perhaps make your copy-constructor throw, and then use `is_nothrow_move_constructible` ? – M.M Nov 26 '15 at 21:44
  • Ah yes, that cannot elide. My bad. Still, the existence of every object can easily be erased under the as-if rule. – Yakk - Adam Nevraumont Nov 27 '15 at 02:20
  • @M.M: -That is a very, very interesting idea!- Unfortunately, if I add my own copy constructor in the first place, there shall be no implicitly generated move constructor. – Irfy Nov 27 '15 at 23:10

3 Answers3

14

Declare the special member functions you want to exist in MyStruct, but don't default the ones you want to check. Suppose you care about the move functions and also want to make sure that the move constructor is noexcept:

struct MyStruct {
    MyStruct() = default;
    MyStruct(const MyStruct&) = default;
    MyStruct(MyStruct&&) noexcept; // no = default; here
    MyStruct& operator=(const MyStruct&) = default;
    MyStruct& operator=(MyStruct&&); // or here
};

Then explicitly default them, outside the class definition:

inline MyStruct::MyStruct(MyStruct&&) noexcept = default;
inline MyStruct& MyStruct::operator=(MyStruct&&) = default;

This triggers a compile-time error if the defaulted function would be implicitly defined as deleted.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Nice. How is it with nothrow; isn't it part of the signature so that it needs to be in the class definition? I seems to recall something was changed in this area (perhaps that was for destructors)? – Johan Lundberg Nov 28 '15 at 09:10
  • @JohanLundberg Right, you declare them `noexcept` in both cases and the compiler will complain if the default version can't be `noexcept`. There's a change that made incompatible exception specifications not cause a hard error when it's defaulted on its first declaration (but rather make the function deleted), but that's not relevant here. – T.C. Nov 28 '15 at 09:32
  • While fiddling with code based on your answer, I realized that members and bases need not have move constructors for `MyStruct` to have one implicitly generated. They merely need be `move_constructible` -- a copy constructor will suffice. Checking whether `MyStruct` defines any special members that would prevent the implicit declaration is trivial, and your answer effectively answers the question, are all members and bases `move_constructible`. – Irfy Nov 29 '15 at 09:50
  • Unfortunately, this answer only added to my confusion overall. 1) Why is the behavior different between inside and outside defaulting of move ctor? 2) When I default it inside, and it would be implicitly deleted, **the code is valid, but the copy ctor is used for move construction**. As if the defaulting inside only forces `MyStruct` to be `move_constructible`, but even then, the copy ctor is used. Was this intentional C++11 design? I made [an elaborate example to test this](https://gist.github.com/Irfy/9e93f166c9d0edb7e4a7) (just toggle the second macro value). – Irfy Nov 29 '15 at 20:11
  • I made [a separate question](http://stackoverflow.com/questions/33988934/why-are-implicitly-and-explicitly-deleted-move-constructors-treated-differently) for my second issue with move semantics, that followed from this question. – Irfy Nov 29 '15 at 22:32
  • Was it intentional to omit the `noexcept` on the move assignment ctor? – wrhall Apr 23 '18 at 15:41
5

As Yakk pointed out, it's often not relevant if it's compiler generated or not.

You can check if a type is trivial or nothrow move constructable

template< class T >
struct is_trivially_move_constructible;

template< class T >
struct is_nothrow_move_constructible;

http://en.cppreference.com/w/cpp/types/is_move_constructible

Limitation; it also permits trivial/nothrow copy construction.

Johan Lundberg
  • 26,184
  • 12
  • 71
  • 97
  • 1
    That limitation, in combination with a non-trivial move constructor is the problem. I am ok with a trivial move constructor -- that is the same thing as a trivial copy constructor. But if it is not trivial, I wish to know if there is actually going to be one. If I define my own copy constructor to make it throw, just so I can use `is_nothrow_move_constructible`, then there will be no implicitly generated move constructor at all. – Irfy Nov 27 '15 at 23:13
1
  1. disable inlining (-fno-inline)
  2. either
    • make sure a move constructor can be used by the code, or (better)
    • temporarily add a call to std::move(MyStruct) anywhere in the compiled code to meet the odr-used requirement
  3. either
    • make sure that MyStruct has at least one parent class or a non-static member (recursively), with a non-trivial move constructor (e.g. an std::string would suffice), or (easier)
    • temporarily add an std::string member to your class
  4. compile/link and run the resultant object file through nm -C ... | grep 'MyStruct.*&&'

The result will imply whether the move constructor was generated or not.


As discussed in the question itself, this method didn't seem to work reliably, but after fixing the two issues that made it unreliable: inlining and triviality of the move constructor, it turned out to be a working method.

Whether the generated move constructor is implicitly or explicitly defaulted plays no role—whether the default is trivial or not is relevant: a trivial move (and copy) constructor will simply perform a byte-wise copy of the object.

Irfy
  • 9,323
  • 1
  • 45
  • 67