Why is a value that takes on the functions of the setter member not recommended in the conversation of Herb Sutter CppCon 2014 (Back to basics: modern C ++ style)?

In Herb Sutter CppCon 2014, let's talk Back to the basics: C ++ modern style, it refers to slide 28 ( web copy of the slides here ) to this template:

class employee { std::string name_; public: void set_name(std::string name) noexcept { name_ = std::move(name); } }; 

He says this is problematic because when calling set_name () with a temporary, noexcept-ness is not strong (he uses the phrase "noexcept-ish").

Now I used the above template quite heavily in my own recent C ++ code, mainly because it saves me by typing two copies of set_name () every time - yes, I know that it can be a little inefficient, forcing to build a copy every time, but hey, I'm a lazy tipter. However, Herb's phrase “This is noexcept problematic” bothers me, because the problem is not solved here: std :: string the forwarding operator is carried over by noexcept, like its destructor, so set_name () above seems to me guaranteed noexcept. I see the potential throw of an exception by the compiler before set_name () as it prepares the parameter, but I try my best to see this as problematic.

Later on slide 32, Herb clearly states that it is an anti-pattern. Can someone explain to me why I did not write bad code while being lazy?

+58
c ++ c ++ 11 stl
Oct 08 '14 at 15:41
source share
4 answers

Others have considered the above noexcept arguments.

Grass spent much more time talking about aspects of efficiency. The problem is not distribution, but the absence of unnecessary exemptions. When you copy one std::string to another, the copy procedure reuses the dedicated storage of the target string if there is enough space to store the data to be copied. When performing a move assignment, you must delete the selected row of the existing memory because it takes the memory from the original row. The idiom of "copy and move" makes the release always happen, even if you do not pass by the temporary. This is a source of terrible performance, which is demonstrated later in the conversation. His advice was to use const ref instead, and if you determine what you need, it has an overload for referencing the r-value. This will give you the best of both worlds: copy to the existing repository for non-temporary, avoiding release and go to the time frame where you are going to pay for release one way or another (either the destination is released before the transition, or the source is freed after copying).

The above does not apply to constructors, since there is no room for free in the member variable. This is good because constructors often take more than one argument, and if you need to do the ref ref / r-value ref for each argument, you get a combinatorial burst of constructor overloads.

Now the question arises: how many classes exist for reusing the repository, for example std :: string when copying? I assume that std :: vector does, but outside of this I am not sure. I know that I never wrote a class that reuses storage, but I wrote a lot of classes containing strings and vectors. Following Herb’s recommendations, you won’t damage classes that won’t reuse memory, you will first copy with the copy version of the receiver function, and if you determine that copying will hit performance too much, do a r-value reference overload to avoid copying (as for std :: string). On the other hand, the use of "copy-and-move" has a demonstrated performance value for std :: string and other types that reuse storage, and these types probably see a lot of code usage by most people. I am following Herb’s advice now, but I need to think about it a bit before I fully consider the issue that the problem has been completely resolved (maybe the blog post that I don’t have time to write is hidden in all this).

+26
09 Oct '14 at 19:45
source share

Two reasons were considered: why passing by value may be better than passing by const.

  • more efficient
  • noexcept

In the case of setters for members of type std::string he refuted the assertion that the transition by value was more efficient, showing that passing by reference to const usually leads to fewer distributions (at least for std::string ).

He also refuted the claim that he allows the setter to be noexcept , showing that the noexcept declaration is misleading, since an exception may still be thrown during the copying of the parameter.

Thus, he concluded that following a const reference should have been preferable to passing by value, at least in this case. However, he mentioned that passing by value was a potentially good approach for designers.

I think that one example for std::string not enough to generalize to all types, but it casts doubt on the practice of passing expensive copies, but cheap for moving parameters at a cost, at least for reasons of efficiency and exclusion.

+8
Oct 08 '14 at 16:43
source share

Herb has a point that accepting a value when you already have dedicated storage can be inefficient and cause unnecessary distribution. But accepting const& almost as bad as if you took a raw C string and passed it to a function, an unnecessary distribution occurs.

What you have to do is an abstraction of reading from a string, not the string itself, because that is what you need.

Now you can do it as template :

 class employee { std::string name_; public: template<class T> void set_name(T&& name) noexcept { name_ = std::forward<T>(name); } }; 

which is quite effective. Then add SFINAE, perhaps:

 class employee { std::string name_; public: template<class T> std::enable_if_t<std::is_convertible<T,std::string>::value> set_name(T&& name) noexcept { name_ = std::forward<T>(name); } }; 

therefore, we get errors in the interface, and not in the implementation.

This is not always practical, as it requires public publication.

There could be a class like string_view :

 template<class C> struct string_view { // could be private: C const* b=nullptr; C const* e=nullptr; // key component: C const* begin() const { return b; } C const* end() const { return e; } // extra bonus utility: C const& front() const { return *b; } C const& back() const { return *std::prev(e); } std::size_t size() const { return eb; } bool empty() const { return b==e; } C const& operator[](std::size_t i){return b[i];} // these just work: string_view() = default; string_view(string_view const&)=default; string_view&operator=(string_view const&)=default; // myriad of constructors: string_view(C const* s, C const* f):b(s),e(f) {} // known continuous memory containers: template<std::size_t N> string_view(const C(&arr)[N]):string_view(arr, arr+N){} template<std::size_t N> string_view(std::array<C, N> const& arr):string_view(arr.data(), arr.data()+N){} template<std::size_t N> string_view(std::array<C const, N> const& arr):string_view(arr.data(), arr.data()+N){} template<class... Ts> string_view(std::basic_string<C, Ts...> const& str):string_view(str.data(), str.data()+str.size()){} template<class... Ts> string_view(std::vector<C, Ts...> const& vec):string_view(vec.data(), vec.data()+vec.size()){} string_view(C const* str):string_view(str, str+len(str)) {} private: // helper method: static std::size_t len(C const* str) { std::size_t r = 0; if (!str) return r; while (*str++) { ++r; } return r; } }; 

such an object can be built directly from std::string or "raw C string" and store what you need to know almost without fail in order to create a new std::string from it.

 class employee { std::string name_; public: void set_name(string_view<char> name) noexcept { name_.assign(name.begin(),name.end()); } }; 

and since now our set_name has a fixed interface (not a perfect forward), it may not implement its implementation.

The only inefficiency is that if you pass a C-style string pointer, you will, of course, go around your size a bit twice (first find '\0' , copy them a second time). On the other hand, it gives your targeted information about how large it is, so it can be distributed in advance, not redistributed.

+5
Oct. 14 '14 at 18:01
source share

You have two ways to call these methods.

  • With the rvalue parameter, if the move constructor of the parameter type does not exist, there is no problem (in the case of std::string , most likely this is not an exception), in any case it is better to use a conditional noexcept (to make sure that the parameters are no exception)
  • In this case, a copy constructor of the parameter type will be called with the lvalue parameter and is almost sure that it will need some selection (which can be selected).

In such cases, when use may be skipped, it is best to avoid. The class client assumes that the exception is not thrown as indicated, but in a valid, compiled, rather than suspicious, C++11 can throw.

+2
Oct 08 '14 at 3:56
source share



All Articles