Item 26: Avoid overloading universal references

Source: Internet
Author: User

This article translated from "effective modern C + +", because the level is limited, it is impossible to guarantee that the translation is completely correct, welcome to point out the error. Thank you!

If you need to write a function that takes a name as a parameter and records the current date and time, add the name to the global data structure in the function. You might think of a function that looks like this:

std::multiset<std::string> name;            // 全局数据结构void logAndAdd(const std::string& name){    auto now =                              // 得到当前时间        std::chrono::system_clock::now();    log(now, "logAndAdd");                  // 产生log条目    names.emplace(name);                    // 把name添加到全局的数据结构中去                                            // 关于emplace的信息,请看Item 42}

This code is not unreasonable, but it can become more efficient. Consider the three possible invocations:

std::string petName("Darla");logAndAdd(petName);                     // 传入一个std::string左值logAndAdd(std::string("Persephone"));   // 传入一个std::string右值logAndAdd("Patty Dog");                 // 传入字符串

In the first call, the parameter name of Logandadd is bound to the PetName variable. In Logandadd, the name is finally passed to Names.emplace. Because name is an lvalue, it is copied to the names. Since the passed-in Logandadd is an lvalue (petname), we have no way to avoid this copy.

In the second call, the name parameter is bound to a right value (the temporary variable-std::string created explicitly by the "Persephone" string). The name itself is an lvalue, so it is copied to names, but we know that, in principle, its value can be move to names. In this call, we made a copy more, but we should have done it with a move.

In the third call, the name parameter is once again bound to an rvalue, but this time it is the temporary variable-std::string created implicitly by the "Patty Dog" string. As with the second call, the name is copied to names, but in this case, the Logandadd primitive argument is a string. If we pass the string directly to emplace, we don't need to create a std::string temporary variable. Instead, within Std::multiset, Emplace will use the string directly to create the Std::string object. In the third invocation, we have to pay the price of copying a std::string, but we really have no reason to pay the price of a move, let alone a copy at a time.

We can eliminate the inefficiency of the second and third calls by rewriting the Logandadd. We make Logandadd a universal reference (see ITEM24) as the parameter, and according to item 25, the reference is Std::forward (forwarded) to Emplace. The result is the following code:

templace<typename T>void logAndAdd(T& name){    auto now = std::chrono::system_clock::now();    log(now, "logAndAdd");    names.emplace(std::forward<T>(name));}std::string petName("Darla");           // 和之前一样logAndAdd(petName);                     // 和之前一样,拷贝左                                        // 值到multiset中去logAndAdd(std::string("Persephone"));   // 用move操作取代拷贝操作logAndAdd("Patty Dog");                 // 在multiset内部创建                                        // std::string,取代对                                        // std::string临时变量                                        // 进行拷贝

Hail! The efficiency is optimal!

If this is the end of the story, I can stop and be proud to leave, but I haven't told you. The client does not always have the name required to access Logandadd directly. Some clients have only one index value, which allows Logandadd to find the appropriate name in the table. To support such a client, the Logandadd is overloaded:

std::string nameFromIdx(int idx);       // 返回对应于idx的namevoid logAndAdd(int idx)                 // 新的重载{    auto now = std::chrono::system_clock::now();    log(now, "logAndAdd");    names.emplace(nameFromIdx(idx));}

For two overloaded versions of a function, the decision to invoke (which function to call) results in the same way as we expected:

std::string petName("Darla");           // 和之前一样logAndAdd(petName);                     // 和之前一样,这些函数logAndAdd(std::string("Persephone"));   // 都调用T&&版本的重载logAndAdd("Patty Dog");                 logAndAdd(22);                          // 调用int版本的重载

In fact, the outcome of the resolution can meet expectations only if you don't expect too much. Suppose a client has an index of type short and passes it to Logandadd:

short nameIdx;...                                     // 给nameIdx一个值logAndAdd(nameIdx);                     // 错误!

The last line of comments is not very clear, so let me explain what happened here.

There are two versions of Logandadd. A version with the Universal reference as a parameter, its T can be deduced to short, resulting in an exact match. A version with an int parameter is only successful if it is converted (that is, type conversion, from small-precision data to a high-precision data type). According to the normal overload function resolution rule, an exact match defeats the match that requires a boost conversion, so the universal reference overload is called.

In this overload, the name parameter is bound to the incoming short value. So name is std::forwarded to 一个std::multiset<std::string> the Emplace member function of names (), and then internally, the name is then forwarded to the Std::string constructor. However, Std::string does not have a constructor with a short argument, so the call to the Std::string constructor in the Multiset::emplace call in the Logandadd call failed. This is because the overload of the universal reference version is a better match for the short parameter than the int version of the overload.

In C + +, a function with a universal reference as a parameter is the most greedy function. They can instantiate an exact match for most of the parameters of any type. (a small subset of the types it cannot match will be described in Item 30.) That's why it's a bad idea to combine overloads with universal references: The overloads of the universal reference version make it much more useless than developers would normally expect.

A simple way to complicate things is to write a perfectly forwarded constructor. A small change to the Logandadd example illustrates the problem. Instead of writing a function that uses std::string or an index (which can be used to view a std::string) as a parameter, we might as well write a person class that can do the same thing:

class Person {publci:    template<typename T>    explicit Person(T&& n)          // 完美转发的构造函数    : name(std::forward<T>(n)) {}   // 初始化数据成员    explicit Person(int idx)        // int构造函数    : name(nameFromIdx(idx)) {}    …private:    std::string name;};

As is the case in Logandadd, passing a shape type other than int (for example, std::size_t, short, long) will not call the int version of the constructor, but instead call the Universal reference version of the constructor, and this will cause the compilation to fail. But the problem here is even worse, because there are other overloads that appear in the person, except what we can see. Item 17 explains that, under appropriate conditions, C + + will produce both the copy and move constructors, even if the class contains a template constructor that instantiates the same function signature as the copy or move constructor. So, if the person's copy and move constructor are generated, the person should actually look something like this:

class Person {public:    template<typename T>                        explicit Person(T&& n)    : name(std::forward<T>(n)) {}    explicit Person(int idx);     Person(const Person& rhs);      // 拷贝构造函数                                    // (编译器产生的)    Person(Person&& rhs);           // move构造函数    …                               // (编译器产生的)};

Only when you spend a lot of time compiling and writing compilers do you forget to think about the problem in the human mind and know that it will lead to a very intuitive behavior:

Person p("Nancy");auto cloneOfP(p);               // 从p创建一个新的Person                                // 这将无法通过编译!

Here we try to create a person from another person, which looks like the case of copying constructors. (P is an lvalue, so we can not consider the "copy" may be done by the move operation). However, this code cannot call the copy constructor. It will call the perfect forwarding constructor. The function then tries to initialize the std::string data member of the person with a person object (p). Std::string does not have a constructor with the person parameter, so your compiler will raise your hand in anger and may express their displeasure with a bunch of incomprehensible error messages.

Why "You might be surprised," isn't it? The perfect forwarding constructor instead of the copy constructor is called? But we are using another person to initialize this person! ”。 We do, but the compiler is defending the C + + rules, and the rules related here are the rules for overloaded functions, which function should be called.

The compiler is for the following reasons: CLONEOFP is initialized with a non-const lvalue (p), and this means that the templated constructor can instantiate a person constructor with a non-const lvalue type parameter. After this instantiation, the person class looks like this:

class Person {public:    explicit Person(Person& n)              // 从完美转发构造函数    : name(std::forward<Person&>(n)) {}     // 实例化出来的构造函数    explicit Person(int idx);               // 和之前一样    Person(const Person& rhs);              // 拷贝构造函数    ...                                     // (编译器产生的)};

In the statement

auto cloneOfP(p);

, p can be passed both to the copy constructor and to the instantiated template. Calling the copy constructor will require the const to be added to P to match the parameter type of the copy constructor, but the invocation of the instantiated template does not require such a condition. So the version generated from the template is a better match, so the compiler did what they had to do: call a more matching function. Therefore, the non-const lvalue of the "copy" of a person type is handled by the perfectly forwarded constructor, not the copy constructor.

If we change the example slightly so that the object to be copied is const, we will get a completely different result:

const Person cp("Nancy");       // 对象现在是const的auto cloneOfP(cp);              // 调用拷贝构造函数!

Because the copied object is now const, it exactly matches the parameters of the copy constructor. A templated constructor can be instantiated into a function with the same signature,

class Person {public:    explicit Person(const Person& n);       //从模板实例化出来    Person(const Person& rhs);              // 拷贝构造函数                                            // (编译器产生的)    ...};

But it doesn't matter, because one of the "overload resolution" rules in C + + is that when a template instance and a non-template function (that is, a "normal" function) are well matched to a function call, the normal function is a better choice. So the copy constructor (a normal function) defeats the instantiated template with the same function signature.

(If you're curious why the compiler can instantiate a copy constructor with a template constructor, they still produce a copy constructor, please review item 17.) )

When inheritance is involved, the relationship between the perfect forward constructor, the compiler-generated copy, and the move constructor becomes more distorted. In particular, traditional derived classes are very strange for the implementation of copy and move operations, so let's take a look at:

class SpecialPerson: public Person {public:    SpecialPerson(const SpecialPerson& rhs)     // 拷贝构造函数,调用    : Person(rhs)                               // 基类的转发构造函数    { … }                                           SpecialPerson(SpecialPerson&& rhs)          // move构造函数,调用    : Person(std::move(rhs))                    // 基类的转发构造函数    { … }                                       };

As the note indicates, the derived class copy and move constructors do not call the copy of the base class and the move constructor, they call the base class's perfect forwarding constructor! To understand why, note that the argument type that the derived class function passes to the base class is the Specialperson type, and then produces a template instance that becomes the overloaded resolution result of the person class constructor. Finally, the code cannot compile because the Std::string constructor does not have a version with Specialperson as the parameter.

I hope that now I have convinced you that overloading the Universal reference parameter is something you should avoid as much as possible. But if overloading a universal reference is a bad idea, what do you do if you need a function to forward different parameter types and you need to do something special for a small number of parameter types? In fact there are many ways to do this, and I'll spend an entire item explaining them, just in item 27. The next chapter is, read on, you will meet.

What you have to remember.
    • Overloading universal references often causes the overloads of the universal reference version to be called more frequently than you expect.
    • The perfect forwarding constructor is the most problematic, because they are often better matches than non-const lvalue, and they hijack the derived class to call the copy of the base class and the move constructor.

Item 26: Avoid overloading universal references

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.