6.2 Special Member Function Templates
6.2 特殊成员函数模板
Member function templates can also be used as special member functions, including as a constructor, which, however, might lead to surprising behavior.
特殊成员函数也可以是模板,例如构造函数。但是这可能会导致令人奇怪的行为。
Consider the following example:
考虑下面的例子:
#include <utility> #include <string> #include <iostream> class Person { private: std::string name; public: // constructor for passed initial name: explicit Person(std::string const& n) : name(n) { std::cout << "copying string-CONSTR for '" << name << "'\n"; } explicit Person(std::string&& n) : name(std::move(n)) { std::cout << "moving string-CONSTR for '" << name << "'\n"; } // copy and move constructor: Person(Person const& p) : name(p.name) { std::cout << "COPY-CONSTR Person '" << name << "'\n"; } Person(Person&& p) noexcept : name(std::move(p.name)) { std::cout << "MOVE-CONSTR Person '" << name << "'\n"; } }; int main() { std::string s = "sname"; Person p1(s); //用string对象初始化 => 调用Person(const string&) Person p2("tmp"); //用字符串字面量初始化 => 调用移动构造函数:Person(string&& n) //Person p3(p1); //拷贝Person对象 => 调用Person(const Person&) Person p4(std::move(p1)); //移动Person对象 => 调用移动构造函数Person(Person&&) }
Here, we have a class Person with a string member name for which we provide initializing constructors. To support move semantics, we overload the constructor taking a std::string:
上例中,Person类有一个string类型的name成员和几个初始化构造函数。为了支持移动语义,我们重载了接受std::string参数的构造函数。
• We provide a version for string object the caller still needs, for which name is initialized by a copy of the passed argument:
提供一个以string对象为参数的构造函数,并用其副本来初始化name成员。
Person(std::string const& n) : name(n) { std::cout << "copying string-CONSTR for ’" << name << "’\n"; }
• We provide a version for movable string object, for which we call std::move() to “steal” the value from:
提供一个以可移动string对象为参数的构造函数,并通过std::move()从中窃取值来初始化name成员:
Person(std::string&& n) : name(std::move(n)) { std::cout << "moving string-CONSTR for ’" << name << "’\n"; }
As expected, the first is called for passed string objects that are in use (lvalues), while the latter is called for movable objects (rvalues):
与预期的一样,传递左值的string对象参数会调用第1个构造函数。而传递可移动对象(右值)则会调用第2个构造函数:
std::string s = "sname"; Person p1(s); // init with string object => calls copying string-CONSTR Person p2("tmp"); // init with string literal => calls moving string-CONSTR
Besides these constructors, the example has specific implementations for the copy and move constructor to see when a Person as a whole is copied/moved:
除了这些构造函数之外,本例中还提供了一个拷贝构造函数和移动构造函数。以查看当传入Person对象时,何时是被复制,何时被移动的。
Person p3(p1); // copy Person => calls COPY-CONSTR Person p4(std::move(p1)); // move Person => calls MOVE-CONSTR
Now let’s replace the two string constructors with one generic constructor perfect forwarding the passed argument to the member name:
现在,让我们将两个string参数的构造函数替换为一个泛型构造函数,它将传入的参数完美转发给name成员。
#include <utility> #include <string> #include <iostream> class Person { private: std::string name; public: // generic constructor for passed initial name: template<typename STR> explicit Person(STR&& n) : name(std::forward<STR>(n)) { std::cout << "TMPL-CONSTR for '" << name << "'\n"; } // copy and move constructor: Person(Person const& p) : name(p.name) { std::cout << "COPY-CONSTR Person '" << name << "'\n"; } Person(Person&& p) noexcept : name(std::move(p.name)) { std::cout << "MOVE-CONSTR Person '" << name << "'\n"; } };
Construction with passed string works fine, as expected:
与预期一样,传入string类型的对象时正常工作:
std::string s = "sname"; Person p1(s); //通过string对象转发 => 调用完美转发构造函数 Person p2("tmp"); //通过字符串字面量初始化=> 调用完美转发构造函数
Note how the construction of p2 does not create a temporary string in this case: The parameter STR is deduced to be of type char const[4]. Applying std::forward<STR> to the pointer parameter of the constructor has not much of an effect, and the name member is thus constructed from a null-terminated string.
注意,现在构造p2时并不会创建一个临时的std::string对象。模板参数STR的类型被推导为char const[4]。但是将std::forward<STR>用于构造函数的指针参数并没有太大的意义,因此name成员将由一个以null结尾的字符串构造的。
But when we attempt to call the copy constructor, we get an error:
但是,当试图调用拷贝构造函数的时候,会出现错误:
Person p3(p1); // ERROR
while initializing a new Person by a movable object still works fine:
而通过一个可移动对象来初始化Person对象则可以正常工作:
Person p4(std::move(p1)); // OK: move Person => calls MOVECONST
Note that also copying a constant Person works fine:
注意,拷贝一个Person的const对象也是没问题的:
Person const p2c("ctmp"); //通过字符串字面量来初始化const对象 Person p3c(p2c); // OK: 拷贝const对象 =>调用Person(const Person&)
The problem is that, according to the overload resolution rules of C++ (see Section 16.2.4 on page 333), for a nonconstant lvalue Person p the member template
问题出在:根据C++重载解析规则(见第333页的16.2.4节),对于非const的左值Person p,成员模板
template<typename STR>
Person(STR&& n)
is a better match than the (usually predefined) copy constructor:
通常比预定义的拷贝构造函数更加匹配:
Person (Person const& p)
STR is just substituted with Person&, while for the copy constructor a conversion to const is necessary.
这里的STR直接被替换成Person&,但是对于拷贝构造函数还需要做一个const转换
You might think about solving this by also providing a nonconstant copy constructor:
为了解决这一问题,你可能会考虑额外提供一个非const版本的拷贝构造函数:
Person (Person& p);
However, that is only a partial solution because for objects of a derived class, the member template is still a better match. What you really want is to disable the member template for the case that the passed argument is a Person or an expression that can be converted to a Person. This can be done by using std::enable_if<>, which is introduced in the next section.
不过,这只是一个部分解决问题的方法。因为对于Person的派生类来讲,成员模板依然会更产生更精确的匹配。你真正想做的是:当传入一个Person对象或者一个可以转换为Person对象表达式时,禁用该成员模板。这可以通过使用std::enable_if<>来实现,它将在下一节中讲到。
【编程实验】重载构造函数与完美转发构造函数时的问题
#include <utility> #include <string> #include <iostream> #include <type_traits> class Person { private: std::string name; public: // generic constructor for passed initial name: template<typename STR> Person(STR&& n) : name(std::forward<STR>(n)) { std::cout << "TMPL-CONSTR for '" << name << "'\n"; } // copy and move constructor: Person(Person const& p) : name(p.name) { std::cout << "COPY-CONSTR Person '" << name << "'\n"; } Person(Person&& p) noexcept : name(std::move(p.name)) { std::cout << "MOVE-CONSTR Person '" << name << "'\n"; } }; class SpecialPerson : public Person { public: using Person::Person; //继承构造函数 //以下两个函数会从父类继承过来,因此无需手动实现。但为了看清楚其声明,特重新 //罗列出来: //拷贝构造函数 //由于sp的类型为SpecialPerson。当调用Person(sp)时,完美构造函数会产生更精确的匹配 //Person(const SpecialPerson&),在这个构造函数中string类型的name成员用子类SpecialPerson //对象初始化,显然会编译失败。注意这里Person的构造函数 //SpecialPerson(const SpecialPerson& sp) : Person(sp) {} //移动构造函数 //SpecialPerson(SpecialPerson&& sp) noexcept: Person(std::move(sp)) {} }; int main() { std::string s = "sname"; Person p1(s); //用string对象初始化 => 调用Person(const string&) //1. 完美构造函数产生更精确的匹配: //Person p2(p1); //error,完美转发构造函数会产生更加精确的匹配Person(Person&),但是在 //该函数中,当初始化name时会执行name(std::forward<Person>(p1)),由于 //name是std::string类型,并不没有提供这样一个通过Person对象来初始化 //的构造函数,因此编译失败。 //2. Person子类的拷贝构造和移动构造: //由于子类构造时,通过Person(sp)/Person(std::move(sp)调用了父类构造函数,此时父类的造 //完美转发函数中将产生一个匹配的构造函数。而在这个函数中会用子类SpecialPerson对象来 //初始化std::string类型的name对象,这显然也是不能通过的。 //SpecialPerson sp("spname"); //SpecialPerson sp1(sp); //SpecialPerson sp2(std::move(sp)); //3.解决方案:就是有条件禁用父类的完美转发构造函数。即当通过Person及其子类对象创建 //对象时,不调用完美转发构造函数,而是转为调用普通的拷贝构造或移动构造函数。 //如将Person的完美构造函数声明为: //template<typename STR, typename = std::enable_if_t < !std::is_base_of_v<Person, std::decay_t<STR>>>> //Person(STR && n) : name(std::forward<STR>(n)) { // std::cout << "TMPL-CONSTR for '" << name << "'\n"; //} return 0; }