第6章 移动语义和enable_if:6.2 特殊成员函数模板

旧时模样 提交于 2020-04-26 08:23:33

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

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!