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;
}