Search code examples
c++pass-by-valueforwarding-referencepass-by-const-referencepass-by-rvalue-reference

Pass-by-value and std::move vs forwarding reference


I encounter the pass by value and move idiom quite often:

struct Test
{
    Test(std::string str_) : str{std::move(str_)} {}
    std::string str;
};

But it seems to me that passing by either const reference or rvalue reference can save a copy in some situations. Something like:

struct Test1
{
    Test1(std::string&& str_) : str{std::move(str_)} {}
    Test1(std::string const& str_) : str{str_} {}
    std::string str;
};

Or maybe using a forwarding reference to avoid writing both constructors. Something like:

struct Test2
{
    template<typename T> Test2(T&& str_) : str{std::forward<T>(str_)} {}
    std::string str;
};

Is this the case? And if so, why is it not used instead?

Additionally, it looks like C++20 allows the use of auto parameters to simplify the syntax. I am not sure what the syntax would be in this case. Consider:

struct Test3
{
    Test3(auto&& str_) : str{std::forward<decltype(str_)>(str_)} {}
    std::string str;
};

struct Test4
{
    Test4(auto str_) : str{std::forward<decltype(str_)>(str_)} {}
    std::string str;
};

Edit:

The suggested questions are informative, but they do not mention the "auto" case.


Solution

  • But it seems to me that passing by either const reference or rvalue reference can save a copy in some situations.

    Indeed, but it requires more overloads (and even worst with several parameters).

    Pass by value and move idiom has (at worst) one extra move. which is a good trade-off most of the time.

    maybe using a forwarding reference to avoid writing both constructors.

    Forwarding reference has its own pitfalls:

    • disallows {..} syntax for parameter as {..} has no type.
      Test2 a({5u, '*'}); // "*****"
      
      would not be possible.
    • is not restrict to valid types (requires extra requires or SFINAE).
      Test2 b(4.2f); // Invalid, but `std::is_constructible_v<Test2, float>` is (falsely) true.
      
      would produces error inside the constructor, and not at call site (so error message less clear, and SFINAE not possible).
    • for constructor, it can take precedence over copy constructor (for non-const l-value)
      Test2 c(a); // Call Test2(T&&) with T=Test2&
                  // instead of copy constructor Test2(const Test2&)
      
      would produce error, as std::string cannot be constructed from Test2&.