Search code examples
c++c++11overloadingforwarding-referenceeffective-c++

Understanding comment from the errata about Item 41 of EMC++


In Item 41, Scott Meyers writes the following two classes:

class Widget {
public:
  void addName(const std::string& newName)   // take lvalue;
  { names.push_back(newName); }              // copy it

  void addName(std::string&& newName)        // take rvalue;
  { names.push_back(std::move(newName)); }   // move it; ...

private:
  std::vector<std::string> names;
};
class Widget {
public:
  template<typename T>                            // take lvalues
  void addName(T&& newName)                       // and rvalues;
  {                                               // copy lvalues,
    names.push_back(std::forward<T>(newName)); }  // move rvalues;
  }                                               // ...
private:
  std::vector<std::string> names;
};

What's written in the comments is correct, even if it doesn't mean at all that the two solutions are equivalent, and some of the differences are indeed discussed in the book.

In the errata, however, the author comments another difference not discussed in the book:

Another behavioral difference between (1) overloading for lvalues and rvalues and (2) a template taking a universal reference (uref) is that the lvalue overload declares its parameter const, while the uref approach doesn't. This means that functions invoked on the lvalue overload's parameter will always be the const versions, while functions invoked on the uref version's parameter will be the const versions only if the argument passed in is const. In other words, non-const lvalue arguments may yield different behavior in the overloading design vis-a-vis the uref design.

But I'm not sure I understand it.

Actually, writing this question I've probably understood, but I'm not writing an answer as I'm still not sure.

Probably the author is saying that when a non-const lvalue is passed to addName, newName is const in the first code, and non-const in the second code, which means that if newName was passed to another function (or a member function was called on it), than that function would be required to take a const parameter (or be a const member function).

Have I interpreted correctly?

However, I don't see how this makes a difference in the specific example, since no member function is called on newName, nor it is passed to a function which has different overloads for const and non-const parameters (not exactly: std::vector<T>::push_back has two overloads for const T& arguments and T&& arguments`, but an lvalue would still bind only to the former overload...).


Solution

  • In the second scenario, when a const std::string lvalue is passed to the template

      template<typename T>
      void addName(T&& newName)
      { names.push_back(std::forward<T>(newName)); }
    

    the instantiation results in the following (where I've removed the std::forward call as it is, in practice, a no-op)

      void addName(const std::string& newName)
      { names.push_back(newName); }
    

    whereas if a std::string lvalue is passed, then the resulting instance of addName is

      void addName(std::string& newName)
      { names.push_back(newName); }
    

    which means that the non-const version of std::vector<>::push_back is called.

    In the first scenario, when a std::string lvalue is passed to addName, the first overload is picked regardless of the const-ness

      void addName(const std::string& newName)
      { names.push_back(newName); }
    

    which means that the const overload of std::vector<>::push_back is selected in both cases.