Should I add a setter with motion on?

This post is a little overclocked before I enter it. I want to clearly say what I'm asking: have you added move-enabled settings to your code and have you found this useful? And how much of my behavior can I expect, maybe it depends on the compiler?

What I see here is whether it is worth adding the set-enabled setter function in cases where I set a property of a complex type. Here I have buttons with support for moving Bar and Foo that have the Bar property that can be set.

 class Bar { public: Bar() : _array(1000) {} Bar(Bar const & other) : _array(other._array) {} Bar(Bar && other) : _array(std::move(other._array)) {} Bar & operator=(Bar const & other) { _array = other._array; return *this; } Bar & operator=(Bar && other) { _array = std::move(other._array); return *this; } private: vector<string> _array; }; class Foo { public: void SetBarByCopy(Bar value) { _bar = value; } void SetBarByMovedCopy(Bar value) { _bar = std::move(value); } void SetBarByConstRef(Bar const & value) { _bar = value; } void SetBarByMove(Bar && value) { _bar = std::move(value); } private: Bar _bar; }; 

In general, in the past I went with the const-ref function for setter functions for non-built-in types. The parameters I was looking at were to pass value-value, then move ( SetByMovedCopy ), pass const-ref, then copy ( SetByConstRef ) and finally take r-value-ref and then move ( SetByMove ). As a baseline, I also included the pass-by-value, and then copied ( SetByCopy ). FWIW, the compiler complained of ambiguity if it included both forwarding values ​​and r-value-ref overloads.

In experiments with the VS2010 compiler, this is what I found:

 Foo foo; Bar bar_one; foo.SetByCopy(bar_one); // Bar::copy ctor called (to construct "value" from bar_one) // Foo::SetByCopy entered // Bar::copy operator= called (to copy "value" to _bar) // Foo::SetByCopy exiting // Bar::dtor called (on "value") 

value copied from bar_one , then value copied to Bar . value destroyed and bears any cost of destroying the complete object. 2 copy operations are performed.

 foo.SetByMovedCopy(bar_one); // Bar::copy ctor called (to construct "value" from bar_one) // Foo::SetByCopy entered // Bar::move operator= called (to move "value" into _bar) // Foo::SetByCopy exiting // Bar::dtor called (to destruct the moved "value") 

value copied from bar_one , then value moves to _bar , then the gutted value destroyed after the function exits, presumably with a lower cost. 1 copy and 1 move operation.

 foo.SetByConstRef(bar_one); // Foo::SetByConstRef entered // Bar::copy operator= called (to copy bar_one into _bar) // Foo::SetByConstRef exiting 

bar_one copied directly to _bar . 1 copy operation.

 foo.SetByMove(std::move(bar_one)) // Foo::SetByMove entered // Bar::move operator= called (to move "value" into _bar) // Foo::SetByMove exited 

bar_one moves directly to _bar . 1 move operation.


Thus, the const-ref and move versions are most effective in this case. Now, moreover, I want to do the following:

 void SetBar(Bar const & value) { _bar = value; } void SetBar(Bar && value) { _bar = std::move(value); } 

What I found happens here, so if you call Foo::SetBar , the compiler selects a function based on whether you pass the value of l or the value of r. You can cause a problem by calling std::move as such:

 foo.SetBar(bar_one); // Const-ref version called foo.SetBar(Bar()); // Move version called foo.SetBar(std::move(bar_one)); // Move version called 

I shudder when I think about adding all these portable setters, but I think that this can lead to a significant increase in performance in cases where the temporary is passed to the SetBar function and in the future I can get even more by using std::move where necessary.

+4
source share
3 answers

tl; dr: Use PassByValue. In your PassByValue, assign via std::move . Use std::move whenever it makes sense to do this when calling setter (i.e. foo.PassByValue(std::move(my_local_var)) ) if you know that the customizer also uses it.

This single version of the installer, taking an object by value, efficiently handles the most common uses, allows the compiler to perform optimizations that are cleaner and more readable.


I like the answers that were given, but I think that the best answer to my question came from the comments in the original question, which make me approach the way I tested these methods from a different angle, so I'm going to this guy gives an answer to my own question.

 class Foo { public: void PassByValue(vector<string> value) { _bar = std::move(value); } void PassByConstRefOrMove(vector<string> const & value) { _bar = value; } void PassByConstRefOrMove(vector<string> && value) { _bar = std::move(value); } void Reset() { std::swap(_bar, vector<string>()); } private: vector<string> _bar; }; 

To check, I compared 3 situations: Passing an l-value, passing an r-value, and passing an explicitly moved l-value as an r-value reference.

The purpose of this test was not to measure the overhead of function calls. This is in the field of micro-optimization. What I'm trying to do is analyze the behavior of the compiler and develop best practices for implementing and using setter functions.

 vector<string> lots_of_strings(1000000, "test string"); Foo foo; // Passing an l-value foo.PassByValue(lots_of_strings); // Passing an r-value foo.PassByValue(vector<string>(1000000, "test string")); // Passing an r-value reference foo.PassByValue(std::move(lots_of_strings)); // Reset vector because of move lots_of_strings = vector<string>(1000000, "test string"); // l-value, calls const-ref overload foo.PassByConstRefOrMove(lots_of_strings); // r-value, calls r-value-ref overload foo.PassByConstRefOrMove(vector<string>(1000000, "test string")); // explicit move on l-value, calls r-value-ref overload foo.PassByConstRefOrMove(std::move(lots_of_strings)); 

Excluded for brevity, I also called Foo::Reset() after each _bar cleanup _bar . Results (after 1000 passes):

 PassByValue: On l-value : 34.0975±0.0371 ms On r-value : 30.8749±0.0298 ms On r-value ref: 4.2927e-3±4.2796e-5 ms PassByConstRefOrMove: On l-value : 33.864±0.0289 ms On r-value : 30.8741±0.0298 ms On r-value ref: 4.1233e-3±4.5498e-5 ms 

Resetting foo after each call may not be the perfect imitation of real life. When I did not do this and instead set _bar to already have some data in place, PassByConstRef performed much better when testing l-value and tested r-value a little better. I believe this was much better when testing l-value, because vector realized that it didn't need to redistribute and just copy the content directly. In the case of moves, however, it will de-distribute independently and bear this cost. But this is a vector specific behavior, and I'm not sure that it should count on much in this context.

Otherwise, the results were similar. The indicated error margin is based only on the standard error of the results and does not take into account the accuracy of the CPU timer used.

The conclusion that I would draw is that it’s better to just pass by value. For this contrived scenario, the two methods were almost identical in terms of productivity and, of course, good enough for the government to work, but the ease of implementation and the clarity of the interface using cross-cutting value give it an edge in my books. I just have to remember to use std::move when calling the setter, when it makes sense to do this, as it can make a big difference.

Hat tip to @Luc_Danton to point me in that direction.

+4
source

Another option would be a template:

 template <typename T> typename std::enable_if<std::is_assignable<Foo, T>::value>::type set(T && t) { foo_ = std::forward<T>(t); } 

This way you can match everything that is converted and any category of values. Remember to #include <type_traits> get is_assignable . (You should not omit enable_if so that your function does not erroneously appear in other symptom checks.)

+7
source

One of the methods I've used is to use macros to automatically create getters / setters for class attributes. Besides the smaller template code, this has other advantages, such as using a macro to automatically provide a typedef and providing consistent interface semantics. I do not find it more difficult to read than other code.

For instance,

 #define ATTRIBUTE(type, name) \ public: \ typedef type name##_type; \ \ void set_##name( type const &p_##name ) { \ m_##name = p_#name; \ } \ \ void set_##name( type &&p_##name ) { \ m_##name = std::move( p_##name ); \ \ type const &get_##name( ) const { \ return m_##name; \ } \ \ type *mutable_##name( ) { \ return &m_##name; \ } \ \ private: \ \ type m_##name; 

Your code now looks something like this:

 struct blah { ATTRIBUTE( std::string, foo ); }; 

I actually think reading is easier than a bunch of setters and getters. (There is a good reason to include a mutable accessory: this means you do not need to make full copies and instead change the member in place, and it is more explicit than a non-constant getter.) Where it gets a little hairy when you use templates as macro arguments, since the preprocessor will be separated by commas, but you can overcome this by specifying the COMMA macro:

 #define COMMA , struct blah { ATTRIBUTE( std::map< foo COMMA bar >, baz ); }; 
-1
source

Source: https://habr.com/ru/post/1413615/


All Articles