Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is a good way to eliminate the boilerplate involved in defaulting/deleting move/copy semantics?

Scott Meyers has a good viewpoint on the rule of zero. Basically he advocates default move/copy assign/construct whether or not you actually need them. Basically the general rule of thumb is to avoid the compiler generation of these members, mainly because they are a big source of confusion (I agree with this).

So I was thinking about a good general practice on how to define a class as default movable, copyable, or non-movable, non-copyable. I thought of boost's boost::noncopyable but I don't like the idea of introducing inheritance for such a functional purpose.

The only thing I can think of that makes sense is to resort to macros. So I came up with something like this:

/// Disable copy construct/assign for the given class T
#define CLASS_NON_COPYABLE(T) \
   T(T const&) = delete; \
   T& operator=(T const&) = delete

/// Disable move construct/assign for the given class T
#define CLASS_NON_MOVABLE(T) \
   T(T&&) = delete; \
   T& operator=(T&&) = delete

/// Disable both copy and move construct/assign for the given class T
#define CLASS_NON_COPYABLE_OR_MOVABLE(T) \
   CLASS_NON_COPYABLE(T); \
   CLASS_NON_MOVABLE(T)

/// Default copy move/assign
#define CLASS_DEFAULT_COPYABLE(T) \
   T(T const&) = default; \
   T& operator=(T const&) = default

/// Default move construct/assign
#define CLASS_DEFAULT_MOVABLE(T) \
   T(T&&) = default; \
   T& operator=(T&&) = default

/// Defaulted versions of both copy and move construct/assign for the given class T
#define CLASS_DEFAULT_COPYABLE_OR_MOVABLE(T) \
   CLASS_DEFAULT_COPYABLE(T); \
   CLASS_DEFAULT_MOVABLE(T)

And an example of how to use them:

class foo
{
public:
    foo() = default;
    virtual ~foo() = default;

    CLASS_NON_COPYABLE(foo);
    CLASS_DEFAULT_MOVABLE(foo);
};

int main()
{
    foo a, b;
    a = b; // FAIL: can't copy; class is "non copyable"
    a = foo(); // OK: class is 'default movable'
}

(Live Sample)

To me this looks much cleaner than the alternative:

class foo
{
public:
    foo() = default;
    virtual ~foo() = default;

    foo(foo const&) = delete;
    foo(foo&&) = default;
    foo& operator=(foo const&) = delete;
    foo& operator=(foo&&) = default;
};

This is debatable, as most style-based issues are, but I find benefits in the former:

  1. The macros are searchable, so you can find classes that utilize move/copy semantics in different ways without using complex regex.
  2. Handle move/copy semantics on older compilers naturally through #ifdef logic where C++11 isn't available (e.g. CLASS_NON_MOVABLE would be no-op).
  3. Easier at the glance of an eye to see what the class is doing.
  4. And easier to type :-)

Given that I am doing a lot of macro magic here, my soul is in a bit of pain and I feel the need for the obligatory "SO post to see if I'm doing this effectively and not going insane".

So I have a few tightly coupled questions:

  1. Am I eliminating boilerplate in an ideal and effective manner?
  2. Are there any better/other or more recommended methods for accomplishing this. I'd even be open to elegant inheritance solutions similar to what Boost provides.

Maybe I'm going about this all wrong. Maybe this whole question is self-defeating and I'm over thinking things. I'd be willing to reconceptualize this whole thing too, just in case I've gone too far down the rabbit hole.

like image 286
void.pointer Avatar asked Oct 03 '14 21:10

void.pointer


People also ask

What does the default move constructor do?

For non-union class types (class and struct), the move constructor performs full member-wise move of the object's bases and non-static members, in their initialization order, using direct initialization with an xvalue argument.

When move constructor is not generated?

The move constructor is not generated because you declared a copy constructor. Remove the private copy constructor and copy assignment. Adding a non-copyable member (like a unique_ptr ) already prevents generation of the copy special members, so there's no need to prevent them manually, anyway.

What is the default copy constructor C++?

Copy Constructors is a type of constructor which is used to create a copy of an already existing object of a class type. It is usually of the form X (X&), where X is the class name. The compiler provides a default Copy Constructor to all the classes.


2 Answers

@HowardHinnant has much better advice for the Rule of Zero:

class foo
{
public:
// just keep your grubby fingers off of the keyboard
};
like image 54
TemplateRex Avatar answered Nov 15 '22 21:11

TemplateRex


I chuckled at and upvoted TemplateRex's good answer. That being said, if you have to declare your destructor virtual, well, then you can't just leave everything to the compiler. However by knowing the rules (and assuming a conforming compiler), one can minimize both what you have to type, and what you have to read.

For this specific example I recommend:

class foo
{
public:
    virtual ~foo()        = default;
    foo()                 = default;
    foo(foo&&)            = default;
    foo& operator=(foo&&) = default;
};

Notes:

  • If you declare either move member, both copy members are implicitly deleted. You can use this rule to reduce boiler plate.

  • I recommend putting your data members up at the top of your class, and then your special members directly following that. This is in contrast to many guidelines which recommend putting your data members at the bottom since they are not important for your readers to see. But if your reader wants to know what the special members are going to do when defaulted, the reader needs to see your data members.

  • I recommend always ordering your declared special members in the same order. This helps the (initiated reader) realize when you have not declared a special member. I have a recommended order. But whatever the order, be consistent.

  • The order I recommend is: destructor, default constructor, copy constructor, copy assignment, move constructor, move assignment. I like this order because I consider the destructor the most important special member. That one function tells me a lot about the class design. I like to group my copy members together, and my move members together, because they are often both defaulted, or both deleted.

As far as the copy members go for this example, given the above style guidelines, it is easy (at least for me) to see that they are implicitly deleted, and so there is less to read (grubby fingers and all that :-)).

Here is a brief paper with more rationale for this style of class declaration.

Update

At the risk of going off topic, it is good practice for foo.cpp to contain confirmation that you got the special members right:

static_assert(std::is_nothrow_destructible<foo>{},
              "foo should be noexcept destructible");
static_assert(std::has_virtual_destructor<foo>{},
              "foo should have a virtual destructor");
static_assert(std::is_nothrow_default_constructible<foo>{},
              "foo should be noexcept default constructible");
static_assert(!std::is_copy_constructible<foo>{},
              "foo should not be copy constructible");
static_assert(!std::is_copy_assignable<foo>{},
              "foo should not be copy assignable");
static_assert(std::is_nothrow_move_constructible<foo>{},
              "foo should be noexcept move constructible");
static_assert(std::is_nothrow_move_assignable<foo>{},
              "foo should be noexcept move assignable");

I've added "nothrow" in some places. Remove it if applicable, or add it to more places if applicable. Instead use "trivially" if applicable. One size doesn't fit all.

This combination of saying what you intend in the header, and confirming what you said is what you got in the source, is very conducive to correct code.

Hiding this "boiler-plate" under macros has a cost: The reader has to look up the definition of the macro. If you use such a macro, judge if the macro's benefit outweighs its cost.

like image 39
Howard Hinnant Avatar answered Nov 15 '22 21:11

Howard Hinnant