Today I read Martinho Fernandes' post,
"Some pitfalls with forwarding constructors." What caught my attention was his claim that a forwarding constructor (i.e., a templatized constructor taking a
universal reference) is a copy constructor. "Can't be true," was my initial thought, because in the past it was true that it couldn't be true. But then I kicked myself. "Crawl out of the 1990s," I told myself (meaning 1998, the year of the first C++ standard). C++11 has new rules, and with new rules comes a new game.
Setting aside edge cases we need not fritter away time on, the copy constructor for a class
C takes a parameter of type
const C&. If there is no such function declared in a class and such a function is needed (because it is used somewhere), it is automatically generated. This is absolutely true in C++98 and is typically true in C++11, and the difference between absolutely and typically is not of interest here. Where I'm going with this does not require acknowledging the nooks and crannies along the way.
In C++98, you could use a template to write what I called a "generalized copy constructor," and you did it like this:
class Widget {
public:
template<typename T>
Widget(const T&);
// ...
};
The key thing to recognize was that this template, though it could be instantiated to yield the signature of a copy constructor, is itself not a copy constructor, and that means that the class does not declare one. That being the case, if the copy constructor is needed, the compiler generates one as usual, because that's the rule: if there is no user-declared copy constructor (i.e., constructor taking a
const Widget& parameter) and one is needed, the compiler generates one. The fact that such a function could be generated from a user-declared template is irrelevant.
When the compiler sees a request to copy an object, it still has to consider the possibility of calling a template instantiation, but the template instantiation has the same signature as the implicitly-generated copy constructor, and (1) the copy constructor is a
function (not a
template function) and (2) when a function and template function are equally good matches for a function call, overloading resolution chooses the function. The end result is that copy constructors can never be generated from templates.
To the best of my knowledge, all this remains true in C++11.
What changes in C++11 is the possibility of writing a generalized copy constructor taking a universal reference--a
universal copy constructor:
class Widget {
public:
template<typename T>
Widget(T&&); // now takes universal reference
// ...
};
Consider now this code:
Widget w;
Widget wcopy = w; // copy w to wcopy
This is a request to copy
w.
Widget has no copy constructor, so the compiler generates one with the default signature. The resulting class looks like this:
class Widget {
public:
Widget(const Widget&); // copy constructor (compiler-generated)
template<typename T>
Widget(T&&); // universal copy constructor
// ...
};
When the compiler see the request to copy
w, it knows it can call the copy constructor, just like it could in C++98, but it also knows that it can instantiate the template. Unlike the C++98 template, however, this one lacks a
const-qualification on its parameter, so the resulting instantiation also lacks one. After instantiation, the class effectively looks like this:
class Widget {
public:
Widget(const Widget&); // copy constructor (compiler-generated)
Widget(Widget&); // instantiated template
// ...
};
Overload resolution now kicks in to choose from these two candidate functions. (Technically, one is a template function, not a function, and to be
really technical about it, it's a template specialization, not a template instantiation, but none of that matters here.)
The parameter to these functions would be the object being copied. That's
w.
w is not
const. A call to the compiler-generated copy constructor would require the addition of
const to get an exact match, but the call to the instantiated template requires no such addition. As a result, it's a better match. The copy constructor will not be called to copy
w. Instead, the instantiated template will.
The situation changes if we copy a
const Widget:
const Widget cw;
Widget cwcopy = cw; // copy a const Widget
Now we're back in C++98-land. The compiler-generated copy constructor and the template instantiation have the same signature (both take
const Widget& parameters), so the function wins.
At least that's how I view it. gcc 4.7 agrees. Given this program,
#include <iostream>
class Widget {
public:
Widget(){};
Widget(const Widget&) { std::cout << "Widget copy ctor "; }
template<typename T>
Widget(const T&) { std::cout << "Generalized Widget copy ctor "; }
template<typename T>
Widget(T&&) { std::cout << "Universal Widget ctor "; }
};
void endLine() { std::cout << '\n'; }
int main()
{
Widget w;
{
std::cout << "Create Widget from Widget:\n";
std::cout << " Direct init w/parens: "; Widget wcopy1(w); endLine();
std::cout << " Copy init : "; Widget wcopy2 = w; endLine();
std::cout << " Direct init w/braces: "; Widget wcopy3 {w}; endLine();
endLine();
}
const Widget cw;
{
std::cout << "Create Widget from const Widget:\n";
std::cout << " Direct init w/parens: "; Widget wcopy1(cw); endLine();
std::cout << " Copy init : "; Widget wcopy2 = cw; endLine();
std::cout << " Direct init w/braces: "; Widget wcopy3 {cw}; endLine();
endLine();
}
}
gcc 4.7 produces this output:
Create Widget from Widget:
Direct init w/parens: Universal Widget ctor
Copy init : Universal Widget ctor
Direct init w/braces: Universal Widget ctor
Create Widget from const Widget:
Direct init w/parens: Widget copy ctor
Copy init : Widget copy ctor
Direct init w/braces: Widget copy ctor
Nothing surprising there, but if I use
auto to deduce the type of the copy, gcc's output changes. That is, this source code,
int main()
{
Widget w;
{
std::cout << "Create auto from Widget:\n";
std::cout << " Direct init w/parens: "; auto wcopy1(w); endLine(); // now using auto
std::cout << " Copy init : "; auto wcopy2 = w; endLine();
std::cout << " Direct init w/braces: "; auto wcopy3 {w}; endLine();
endLine();
}
const Widget cw;
{
std::cout << "Create auto from const Widget:\n";
std::cout << " Direct init w/parens: "; auto wcopy1(cw); endLine();
std::cout << " Copy init : "; auto wcopy2 = cw; endLine();
std::cout << " Direct init w/braces: "; auto wcopy3 {cw}; endLine();
endLine();
}
}
produces this output:
Create auto from Widget:
Direct init w/parens: Universal Widget ctor
Copy init : Universal Widget ctor
Direct init w/braces: Universal Widget ctor Universal Widget ctor
Create auto from const Widget:
Direct init w/parens: Widget copy ctor
Copy init : Widget copy ctor
Direct init w/braces: Widget copy ctor Universal Widget ctor
This
is surprising. Where are those extra constructor calls coming from? (In a comment on my
last post, Julain posted results for clang, which did not show such extra calls.)
I had a theory. To test it, I added a move constructor to
Widget,
class Widget {
public:
Widget(){};
Widget(const Widget&) { std::cout << "Widget copy ctor "; }
Widget(Widget&&) { std::cout << "Widget move ctor "; } // added this
template<typename T>
Widget(const T&) { std::cout << "Generalized Widget copy ctor "; }
template<typename T>
Widget(T&&) { std::cout << "Universal Widget ctor "; }
};
then reran the test (using
auto-declared variables). The revised results?
Create auto from Widget:
Direct init w/parens: Universal Widget ctor
Copy init : Universal Widget ctor
Direct init w/braces: Universal Widget ctor
Create auto from const Widget:
Direct init w/parens: Widget copy ctor
Copy init : Widget copy ctor
Direct init w/braces: Widget copy ctor
No extra constructor calls. Funky, no?
Some of the following analysis is incorrect, but rather than simply edit the post to remove the wrong information and replace it with correct information, I'll add comments like this.
gcc is making an extra call to the universal copy constructor only when I use brace initialization. My theory is based on the observation that when brace initialization is used to initialize a
std::initializer_list object, the values in the braced list are copied into a temporary rvalue array. There are no
std::initializer_list objects in my code, but let's suppose, for reasons I cannot fathom, that the use of
auto in the above code somehow prompts gcc to treat a braced initializer as if it were being used with a
std::initializer_list.
There are std::initializer_list objects in my code, because, as a prospective guideline for Effective C++11 puts it, "Remember that auto + {} == std::initializer_list." The statement
auto wcopy {w};
creates an object of type std::initializer_list<Widget>, not, as I had thought when I wrote the original post, an object of type
Widget.
If that were the case, the to-be-copied
Widget would be copied into the array, where it would be an rvalue that would then be moved into the object being initialized. That is, given
auto wcopy {w};
w would be copied into an array, and a request would then be made to move this array element into
wcopy. But in the code where I declare no move constructor, no move constructor can be generated, because I've declared a copy constructor, and if you declare a copy operation, no move operators are generated. So in the code that requests a move of the rvalue in the array, the move is accomplished by calling a copy operation.
Because the object in the array is a
copy of the actual initialization argument, it's not
const, even if what it's a copy of is. In this code,
const Widget cw;
auto cwcopy {cw};
though
cw is const, the copy of
cw in the array is not. That means that when the compiler has to choose between the copy constructor (which takes a
const Widget& parameter) and the instantiated universal copy constructor (which, in this case, takes a
Widget& parameter), the universal copy constructor is the better match.
Remember, this is all a theory. But it does explain the output from gcc. In particular, it explains:
- Why there is an extra call to a copying operation: because the first copy operation copies the initializing object into the array, and the second one copies it from the array to the target of the initialization.
- Why the extra call is to the universal copy constructor: because any cv qualifiers present on the initializing object are absent from the copy in the array, and the universal copy constructor is a better match than the conventional copy constructor for non-const objects.
- Why the extra call goes away when I declare a move constructor: because then the move constructor can be called on the rvalue in the array, but calls to copy constructors and move constructors can be optimized away under certain conditions (which are fulfilled here). Such call elision is generally not permitted for other functions, including the universal copy constructor. Which means that when the move constructor is missing and cannot be generated, the universal constructor must be called, but when the move constructor is present, it need not be called.
Are we having fun yet?
If my theory is correct, I think it identifies a bug in gcc, because, as far as I know, the only time the contents of a braced initializer list are copied into a temporary array is when they're being used with a
std::initializer_list object. (It may happen in calls to
std::async and the variadic
std::thread constructor, too, but I'm too lazy to look it up right now.)
I no longer believe that this is a bug in gcc, because gcc is doing exactly what I'd guessed: initializing a std::initializer_list<Widget> object. I just didn't think it was supposed to. Now I do.
Anyway, bottom line: in C++98, copy constructors could not be generated from templates, but in C++11, they can be, practically speaking. At least that's my current understanding. Thanks to Martinho Fernandes for
his post that sent me down this particular rabbit hole.
Thanks also to Xeo for pointing out the error in my original post.
Scott