8

I'd like to detect (and use the result in std::enable_if) whether a C++ class has a move constructor defined.

The following program prints MOVE, so using std::is_move_constructible is not the way to do it:

#include <stdio.h>
#include <type_traits>
class C {
 public:
  C() { puts("C()"); }
  C(int) { puts("C(int)"); }
  ~C() { puts("~C()"); }
  C(const C&) { puts("C(const C&)"); }
  // C(C&&) { puts("C(C&&)"); }
  C& operator=(const C&) { puts("C="); return *this; }
};
int main(int argc, char** argv) {
  (void)argc; (void)argv;
  if (std::is_move_constructible<C>::value) puts("MOVE");
  return 0;
}

I need a program which prints MOVE only if I uncomment the line containing the &&.

pts
  • 80,836
  • 20
  • 110
  • 183
  • 2
    you want "has_user_defined_move_ctor" basically? – Borgleader Feb 23 '17 at 18:15
  • 7
    Since an rvalue can bind to const reference, the distinction isn't visible to the class's users unless you actually delete the rvalue constructor, i.e. `C(C&&) = delete;`. Do you have a genuine problem that's solved by knowing whether there's a move constructor? – Toby Speight Feb 23 '17 at 18:17
  • Yes, I want has_user_defined_move_ctor. The genuine problem is efficiently appending to an std::vector: if the class has a move constructor, I want to call push_back; otherwise, if the class has swap, I want to call resize + back + swap, otherwise I want to call push_back (only this last-resort push_back is slow, because it is doing a large copy). I want to make it work for classes in C++98 libraries (to which I'm unable to add missing move constructors without changing a their source code, and that would be cumbersome). – pts Feb 23 '17 at 19:10
  • @TobySpeight: If you add your comment about `= delete` as an answer, I'll accept it. FYI I've tried with `= delete`, and std::is_move_constructible indeed returns false in that case. – pts Feb 23 '17 at 19:12
  • 1
    @pts `push_back()` uses a move if the input is movable, otherwise it uses a copy. Also look at `emplace_back()`. And move semantics did not exist in C++98, they were introduced in C++11. – Remy Lebeau Feb 23 '17 at 20:16
  • @RemyLebeau: Sure, I know all this. But how does my code automatically do a swap if the move constructor is not available, but swap is? For that we'd need to be able to detect the move constructor, and I'm looking for advice on that in this question. – pts Feb 23 '17 at 22:15
  • @RemyLebeau: Please note that I'm using some libraries from my C++11 code. The libraries were written in C++98, thus they don't have move constructors. If possible, I'd like to avoid modifying the libraries. – pts Feb 23 '17 at 22:23
  • 2
    @RemyLebeau That's actually not quite correct, it will only use move if it is no-throw movable. Otherwise `push_back` will copy. This is to maintain the strong exception guarantee; if you do moves and they can throw, if one of the moves throws you cannot restore the original state without risking another throw. – Nir Friedman Feb 23 '17 at 22:50
  • @pts When you say "if the class has swap", do you mean if the class has member swap? The thing is that member swap is of course easy to detect, and member swap is definitely a deprecated idiom that should not be used in modern C++. So you can change your order to first detect member swap, and then just do push_back, I think in practice this will handle both new and old classes efficiently. – Nir Friedman Feb 23 '17 at 22:54
  • @NirFriedman: Yes, member swap. Please note that push_back with the move constructor has smaller overhead (i.e. the additive constant is smaller) than resize + back + swap, that's why I'd like to use the move constructor if possible. Most of my new classes do have a move constructor, and I'd like to automatically use push_back on them. – pts Feb 23 '17 at 23:13
  • 1
    @ptss Right, what i'm saying is that your new classes should not have member swap (if they do, then change that; member swap is not best practice). So as long as your new classes don't have member swap, you can just check for swap first, and then fall back to push_back. It's not the ideal order but it will still be correct. – Nir Friedman Feb 24 '17 at 02:58
  • @NirFriedman: The most important problem with detecting member swap is that STL classes and templates such as std::vector and std::string (and 16 more) have member swap, and I still want their move destructor be called instead of their member swap. It's not easy to detect them via type traits, because they have many template arguments. – pts Feb 28 '17 at 11:37
  • @pts That's a good point. This is a legitimately tough nut. – Nir Friedman Feb 28 '17 at 15:46
  • 1
    @NirFriedman: I ended up using a workaround: checking for std::get, ->shrink_to_fit, ->emplace and ->remove_suffix. If any of them work, I don't use member swap, and assume that the move constructor is efficient. All these functions and methods were introduced in C++11, so if they are present, then it's probably a C++11 STL class. https://github.com/pts/fast_vector_append/blob/f67771c93915c8c4f1ed0142aca792103518e665/fast_vector_append.h#L101 – pts Feb 28 '17 at 18:26

1 Answers1

10

Short answer: it's impossible.

Some more details, based on the comment by @TobySpeight:

If the class doesn't contain C(C&&) = delete;, then it's impossible to detect whether it contains C(C&&) { ... } or not: std::is_move_constructible<C>::value will be true in either case, and there is no other way to detect it.

The presence of C(C&&) = delete; can be detected: std::is_move_constructible<C>::value is false iff C(C&&) = delete; is present.

More explanation in this answer to "Understanding std::is_move_constructible".

To avoid slow copying in std::vector::push_back, detecting a user-defined move constructor is not necessary. This is the alternative, based on @NirFriedman's comment:

  • All classes have a copy constructor.
  • Old (C++98) classes have a member swap and 0-argument constructor (can be implicitly generated by the compiler), and they don't have a user-defined move constructor.
  • New (C++11) classes don't have a member swap (but they can have a namespace-level swap or a friend swap), and they have a user-defined move constructor.
  • Small classes (for which we don't care about the speed of the copy constructor, it's always fast enough) may have a user-defined move constructor. They also may have a member swap (but they shouldn't, for speed). If they have a member swap, they also have a 0-argument constructor (can be implicitly generated by the compiler).
  • SFINAE is used to to detect the member swap.
    • If the member swap is present, resize + back + swap is used to add a new element to the vector.
    • Otherwise, push_back is used.

For old classes, this will use the member swap (fast). For new classes, it will use the move constructor (fast, also a little bit faster). For small classes, it will use the copy constructor (assumed to be fast enough for small classes) or the move constructor (if available).

Here is my final solution for a fast std::vector::push_back with member swap detection: https://github.com/pts/fast_vector_append/blob/master/fast_vector_append.h

pts
  • 80,836
  • 20
  • 110
  • 183
  • 1
    "it's impossible"... [Or is it?](https://stackoverflow.com/questions/51901837/how-to-get-if-a-type-is-truly-move-constructible/51912859#51912859) ;) – C.M. Feb 22 '22 at 16:09