Search code examples
c++c++11constructormove

Efficient way to construct object with string member


Suppose I have a class ThisHasAStringMember. Assume it has a private member which is a string, and I want to efficiently get the string value, preferring a move over a copy where possible. Would the following two constructors accomplish that?

class ThisHasAStringMember
{
public:
  // ctors
  ThisHasAStringMember(const std::string str) : m_str(str) {}
  ThisHasAStringMember(std::string &&str) : m_str(std::move(str)) {}

  // getter (no setter)
  std::string value() { return m_str; }
private:
  std::string m_str;
}

Do I need the double ampersand before the str parameter in the second constructor?

Is this the right way to accomplish this?


Solution

  • At first I would notice that it's better to mark your constructors as explicit.

    The next moment is that better change the first constructor in your solution to take const reference to avoid copying lvalue:

    // ctors
    ThisHasAStringMember(const std::string& str) : m_str(str) {}
    ThisHasAStringMember(std::string &&str) : m_str(std::move(str)) {}
    

    This approach is optimal from the performance point of view (you will have one copy constructor call for lvalue and one move constructor call for rvalue), however it's quite boring to implement each time two constructors in such case. And if you have N members - 2^N constructors.

    There are few alternatives:

    1. Signle constructor where you pass parameter just by value. Yes it was unefficient in C++98, but in C++11 when you create a full copy - that's an option.

      ThisHasAStringMember(std::string str) : m_str(std::move(str)) {}

    When lvalue is passed there will be one copy constructor call and one move constructor call. When rvalue is passed there will be two move constructor calls. Yes, you have one extra move constructor call in each of the cases. But it's often very cheap (or even can be optimized away by compiler) and the code is very simple.

    1. Single constructor where you pass parameter by rvalue:

      ThisHasAStringMember(std::string&& str) : m_str(std::move(str)) {}

    If you pass lvalue you have to explicitely copy it first in a place of the call, e.g. ThisHasAStringMember(copy(someStringVar)). (here copy is a simple template copying method). And you will still have one extra move constructor call for lvalues. For rvalues there will be no overhead. Personally I like this approach: all the places where the parameter is copied are explicit, you won't make occasional copies in a performance-critical places.

    1. Make constructor template and use perfect forwarding:
    template <typename String, 
        std::enable_if_t<std::is_constructible_v<std::string, String>>* = nullptr>
    ThisHasAStringMember(String&& str) : m_str(std::forward<String>(str))
    {}
    

    You will have no overhead both for rvalues and lvalues, but you'll need to make your constructor template and define it in header in most of the cases.