2

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.

Alex O
  • 1,429
  • 2
  • 13
  • 20
  • Does this answer your question? [Is the pass-by-value-and-then-move construct a bad idiom?](https://stackoverflow.com/questions/21035417/is-the-pass-by-value-and-then-move-construct-a-bad-idiom) – Quimby Oct 27 '21 at 16:51
  • 1
    This is analyzed in detail in "Modern C++" textbook by Scott Meyers. – Eugene Oct 27 '21 at 16:55
  • @康桓瑋 No, it is a different question. – Eugene Oct 27 '21 at 19:18

2 Answers2

3

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&.
Jarod42
  • 203,559
  • 14
  • 181
  • 302
1

Adding to the answer by Jarod42 and the proposed dupes(1), you can overcome the shown pitfalls of the forward reference approach by restricting the valid types of a template parameter pack.

#include <string>
#include <concepts>

struct Test
{
    template<class... Args>
        requires std::constructible_from<std::string, Args...>
    Test(Args&&... str_)
        : str( std::forward<Args>(str_)... )
    {}
    std::string str;
};

int main()
{
    Test a{"So far, so good..."};

    Test b{5u, '*'};      // -> "*****"

//    Test b({5u, '*'});  // It works too.
    
    Test c{b};

//    Test d(4.2f);
// error: no matching constructor for initialization of 'Test'
}

(1) Is the pass-by-value-and-then-move construct a bad idiom? and
Advantages of pass-by-value and std::move over pass-by-reference

Bob__
  • 12,361
  • 3
  • 28
  • 42