I recently went through Sumant Tambe's presentation materials from his Silicon Valley Code Camp presentation, "C++11 Idioms." He argues that an emerging idiom is to pass arguments to constructors by value, because this takes advantage of move opportunities when they are available. I was surprised to read about this emerging idiom, in part because I had not heard of it (I'm supposed to be clued in about this kind of stuff) and in part because it runs contrary to my own thinking on the subject, which is to use perfect forwarding.
His presentation goes on to discuss the general problem of efficient parameter passing, based on a trio of blog posts by
Suppose I have a Widget class with a std::string data member. In C++98, I'd initialize it like this:
class Widget1 {
public:
Widget1(const std::string& n): name(n) {}
private:
std::string name;
};
The problem here is that if an rvalue is passed to the Widget1 constructor, name is copy-initialized from n instead of being move-initialized. That's easy to fix using perfect forwarding, which is designed to optimize this kind of thing:
class Widget2 {
public:
template <typename T>
Widget2(T&& n): name(std::forward<T>(n)) {}
private:
std::string name;
};
No muss, no fuss, no enable_if, no testing for whether the argument passed to n was an rvalue--none of that stuff. Very nice.But let's suppose there's another way to initialize a Widget. Instead of passing a name, we can pass an ID number, and the name can be looked up from the ID number. In C++98, adding this would be trivial:
class Widget3 {
public:
Widget3(const std::string& n): name(n) {}
Widget3(int id): name(findNameFromID(id)) {}
private:
std::string name;
};
Adding this to the version of Widget with the perfect forwarding constructor, however, leads to grief. But not immediately. The class will compile without any trouble, and simple tests will work as expected:class Widget4 {
public:
template <typename T>
Widget4(T&& n): name(std::forward<T>(n)) { std::cout << "T&&\n"; }
Widget4(int id): name(findName(id)) { std::cout << "int\n"; }
private:
std::string name;
};
Widget4 w1("Hello"); // calls perfect forwarding ctor
Widget4 w2(22); // calls int constructor
But slightly trickier tests fail. For example, if the type of the passed ID isn't exactly int--e.g., a long or a short or an unsigned--the perfect forwarding constructor is a better match, and the code will try to initialize a std::string with a numerical value. This makes no sense, and the compiler will exhibit no reluctance in telling you that. Given
Widget4 w3(22L); // pass a longgcc 4.7 says this:
ctors.cpp: In instantiation of 'Widget4(long int &&)':
ctors.cpp:40:18: required from here
ctors.cpp:18:42: error: invalid conversion from 'long int' to 'const char *' [-
fpermissive]
basic_string.h:487:7: error: init. arg 1 of 'basic_string<
char; _Traits = char_traits<char>; _Alloc = allocator<char>, _Traits
, _Alloc
>::basic_string(
const char; _Traits = char_traits<char>; _Alloc = allocator<char> *
, const _Alloc &
)' [-fpermissive]
In file included from c:\users\scott\apps\mingw\bin\../lib/gcc/i686-pc-
mingw32/4.7.0/../../../../include/c++/4.7.0/string:54:0,
from ctors.cpp:1
The fundamental problem is that perfect forwarding and overloading make very bad bedfellows, because perfect forwarding functions want to take everything. They're the greediest functions in C++.To curb their avarice, you have to limit the conditions under which they can be instantiated, and that's where enable_if comes in. And where enable_if comes in, any semblance of beauty leaves. For example, if you'd like to disable the perfect forwarding template for integral types (thus allowing the int overload to handle those types), you could code it this way:
class Widget5 {
public:
template <typename T>
Widget5(T&& n, typename std::enable_if<!std::is_integral<T>::value>::type* = 0)
: name(std::forward<T>(n)) { std::cout << "T&&\n"; }
Widget5(int id): name(findName(id)) { std::cout << "int\n"; }
private:
std::string name;
};
This code behaves as we'd like, assuming (sigh) we'd like to treat a char as a number:
Widget5 w3a("Sally"); // calls perfect fowarding ctor
Widget5 w3b(44); // calls int ctor
Widget5 w3c(44L); // calls int ctor
Widget5 w3d((short)44); // calls int ctor
Widget5 w3e('x'); // calls int ctor
If more constructor overloads are added, or if the constraints on when the perfect forwarding constructor is applicable become more complicated, the enable_if-related code can become, er, unpleasant. Such unpleasantness is presumably the noise that
Sumant Tambe refers to when he rejects perfect forwarding.In those cases, I think his preferred solution to the problem of declaring constructor parameters--take everything by value--is reasonable. The generated code isn't quite as efficient as you'd get from perfect forwarding, but the overloading rules are a lot easier to understand, and the resulting code is easier to both read and write.
I still think that perfect forwarding is the language tool you should prefer when you need to write constructors and similar functions (e.g., setters) that simply shuttle values from one place to another. That's what it's designed for. If you need to overload such functions, however, things get very messy very quickly, and except in the most demanding of performance-sensitive applications and libraries, I think it's reasonable to fall back on pass-by-value as the parameter-passing mechanism.
But these are still early days in C++11 programming, and we're all still learning. I welcome your thoughts on how to declare constructor parameters, the proper role of perfect forwarding, and the wisdom of passing parameters by value.
Scott











