may contain source code

published: • tags:

Question: Is this a copy constructor? Don’t look it up!

T::T(T& other, int i = 0);

I thought I knew this! It has two parameters and the first one isn’t const. This is not a copy constructor. To my surprise C++ disagrees. I should have expected something like this, though. In C++ everything is more subtle than it looks.

Of course the next question was: What exactly does count as a default/copy/move constructor, copy/move assigment operator or destructor? The C++ standard calls this group of functions special member functions and dedicates a whole chapter to them. It has the ID [special] and is chapter 15 in C++17. That’s the standard I’ll be referring to in the rest of the article.

We’ll take a close look at the allowed signatures of the special functions and at the rules for when the compiler provides some or all of them implicitly.

For the Impatient

These are the things I found most notable:

  • Default/copy/move constructors can have additional parameters as long as they all have a default argument.
  • The first parameter of all the copy/move functions can be unqualified or have any combination of const and volatile.
  • Declaring a copy/move constructor prevents the compiler from generating the implicit default constructor.

Signatures

The rules from the standard in a nutshell:

  • The first argument of the copy/move functions of a type T:
    • must be of type T
    • must be an lvalue reference for copy (T&) and and rvalue reference for move (T&&); there’s one special case for copy, see below
    • can be const or volatile in any combination
  • The default/copy/move constructors can have any number of additional parameters as long all of them have default arguments.
  • The return type of the copy/move assignments isn’t restricted. T& is a strong convention, but not a law.
  • Copy/move assignments must be non-static member functions.
  • Except default constructors none of the special member functions can be templates.

Let’s look at this in code.

struct T
{
    // examples for valid default constructors (15.1(4))
    T();
    T(int i = 1, bool b = true);
    template <typename X = int> T(const X& = X{});

    // examples for valid copy constructors (15.8.1(1))
    T(const T&);
    T(T&);
    T(volatile T&);
    T(const volatile T&);
    T(const T&, int i = 1);

    // examples for valid copy assignments (15.8.2(1))
    T& operator=(const T&);
    T& operator=(T&);
    T& operator=(volatile T&);
    T& operator=(const volatile T&);
    T& operator=(T);  // this is the special case mentioned above
    void operator=(const T&);

    // examples for valid move constructors (15.8.1(2))
    T(T&&);
    T(const T&&);
    T(volatile T&&);
    T(const volatile T&&);
    T(T&&, int i = 1);

    // examples for valid move assignments (15.8.2(3))
    T& operator=(T&&);
    T& operator=(const T&&);
    T& operator=(volatile T&&);
    T& operator=(const volatile T&&);
    void operator=(T&&);

    // only possible form of a destructor (15.4)
    ~T();
};

And here are some counter examples. They are valid C++, but not special member functions. This has two major consequences:

  • When looking for a certain special member function to call implicitly, the compiler will ignore the examples below. You might get a compilation error about, say, a missing default constructor even though at first glance it looks like one is implemented.
  • When deciding whether to generate the implict version of a special member function the compiler will ignore the ones below. That’s potentially dangerous because it can lead to code that compiles but doesn’t do what you expect.
struct T
{
    // not a default ctor
    // cannot be called without function argument or explicit template argument
    template <typename X> T(X x = X{});

    // not copy/move ctors
    // additional parameters must have default arguments
    T(const T&, int);
    T(T&&, bool);

    // first parameter is not of type T
    T(const X&);
    T(X&&, int i = 0);

    // templates not allowed (even if X is T)
    template <typename X> T(const X&);
    template <typename X> T(X&&);

    // not copy/move assignments
    // parameter is not of type T
    T& operator=(const X&);
    T& operator=(X&&);

    // templates not allowed (even if X is T)
    template <typename X> X& operator=(const X&);
    template <typename X> X& operator=(X&&);
};

Then there’s a notable compilation error. Remember that copy assignment can take a T by value? Well, a constructor cannot.

struct T
{
    // ill-formed according to 15.8.1(5)
    T(T);
};

The rules seem a bit wild at first glance, but I think there is good reason for most of them.

  • The purpose of a default constructor is to construct an object without providing any additional information. Accordingly it must be callable without explicitly passing any arguments. That’s still possible if all its parameters have default arguments.
  • The situation is similar for the copy/move functions. They do need some additional information: another object to copy or move. So they must be callable with a single argument of their own class’s type. Again, additional parameters with default arguments don’t hurt. That the copy/move assignments only allow a single parameter is unrelated to their role as special member functions. They’re operators, and the number of parameters is determined by the associated syntax. After all, how would you pass additional arguments to x = y?
  • When copying/moving the cv-qualification of the other object often doesn’t matter. Nothing changes for a copy constructor, whether its parameter is const or not. But corner cases may exist, where the ability to implement both those copy constructors is useful. Of course, for each copy/move function there is a cv-qualification that makes the most intuitive sense, which leads to the widely used signatures everyone is familiar with.

Compiler Provided Functions

The special member functions aren’t only special because of their role in creating and destroying objects. Also, in certain situations the compiler implicitly provides an implementation for some or all of them. You can influence these declarations with =default and =delete.

The functions implicitly provided by the compiler are always public, inline and non-explicit. Here are the signatures.

// signatures of the compiler provided special member functions
struct T
{
    T();                     // default constructor (15.1(4))

    T(const T&);             // usual copy constructor (15.8.1(6) and (7))
    T(T&);                   // alternative copy constructor

    T& operator=(const T&);  // usual copy assignment (15.8.2(2))
    T& operator=(T&);        // alternative copy assignment

    T(T&&);                  // move constructor (15.8.1(8) and (9))
    T& operator=(T&&);       // move assignment (15.8.2(4) and (5))

    ~T();                    // destructor (15.4(4))
};

Note the subtleties for copy construction and copy assignment. By default the const versions are generated. But if T has any base classes or any member variable types with a non-const copy constructor/assignment then the compiler switches to the non-const version for T, too.

When you write a class and do not declare any of the special member functions, then usually the compiler implicitly generates the full set. However, quite a few things can lead to some of the implicit functions to be suppressed.

  • Non-static const member variables disable copy/move assignment.
  • Non-static reference member variables disable copy/move assignment.
  • When a base class or non-static member variable does not provide a certain function, it is disabled in your class as well.
  • When you add your own implementation of one of the special functions, the compiler might not implicitly generate some of the others. The declaration alone counts. It does not matter whether you fully define, =default or =delete it. The table below shows the exact rules for this kind of suppression.
compiler provided function
  if you declare
ctor copy ctor copy op= move ctor move op= dtor
default ctor suppressed suppressed suppressed
copy ctor suppressed suppressed
copy op= suppressed suppressed
move ctor suppressed suppressed suppressed suppressed
move op= suppressed suppressed suppressed suppressed
dtor

Comments