3

Below is a class with one string member. We’d like to initialise it in the constructor:

class MyStr { std::string m_str; public: MyStr(const std::string& rstr) : m_str(rstr) {} }; 

The constructor takes const std::string&.

We could replace a constant reference with string_view:

MyStr(std::string_view strv) : m_str(strv) {} 

Or pass a string by value and move from it:

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

Which of the alternatives is preferred?

3 cases:

MyStr mystro1{"Case 1: From a string literal"}; std::string str2 { "Case 2: From l-value"}; MyStr mystro2 { str2 }; std::string str3 { "Case 3: From r-value reference"}; MyStr mystro3 { std::move(str3) }; 
1
  • 5
    It depends on what you want to do. All are valid ways to initialize string. "Better" is relative. Better performance ? Better code lisibility ? Commented Aug 8, 2018 at 14:08

4 Answers 4

2

The most efficient way is to perfect-forward any argument convertible to std::string:

class MyStr { std::string m_str; public: template<class T, class = std::enable_if_t<std::is_convertible<std::remove_reference_t<T>, std::string>::value>> MyStr(T&& arg) : m_str(std::forward<T>(arg)) {} }; 

A bit of a mouthful though. The second best option is to not provide constructors at all, make data members public and initialize MyStr with aggregate initialization syntax.

Full story: CppCon 2017: Nicolai Josuttis “The Nightmare of Move Semantics for Trivial Classes”.

Sign up to request clarification or add additional context in comments.

6 Comments

And the most legible is... Well, pretty much anything else ;)
I'd leave off the enable_if unless it was necessary
@Caleth Without enable_if the constructor is going to be used for things not convertible to strings.
No, it'll be a hard error to try. It is currently a hard error to try, so nothing has changed. You remove one level of template depth in the error message with the enable_if
@Caleth When other constructors are added this constructor is going to be chosen over the other ones. This is why it needs to be restricted on what it accepts. Watch that video to get more insight.
|
1

Here is a practical rule of thumb with a more general scope:

Pass all cheaply movable sink arguments by value and use std::move whenever appropriate.

Prime examples for types which are not cheaply movable: std::array and other types T where sizeof(T) is "large"

This is generally a good compromise between usability (on either side of the interface) and computational overhead. Typically, the extra work (compared to perfect forwarding) is at most one additional move construction and at most one additional deconstruction. Unless you have identified a performance bottleneck of your application, you most probably do not want to spend more programming effort.

This blog post on how to pass sink arguments includes a longer discussion on the alternatives, and its conclusion also starts with "stick to passing by value" as the default.

Applied to your std::string member example this suggests

class MyStr { std::string m_str; public: MyStr(std::string rstr) : m_str(std::move(rstr)) {} }; 

In addition to the general performance aspects mentioned above, note that the signature MyStr::MyStr(std::string rstr) implies that MyStr is storing a real copy of the thing passed as rstr. This may help a user to understand how MyStr should be used.

Comments

0

As clonk said, you need to specify what do you want exactly. Are you looking for performance or something else?

Also you can compare this yourself using the compiler-explorer

https://godbolt.org/.

Write the code in the compiler-explorer and it will automatically generate the assemble code. Of course, you have choice of selecting the compiler.

1 Comment

This doesn't really answer the question. It should've been a comment.
0

A full answer: Analysing the Cases

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.