Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Use of rvalue references in function parameter of overloaded function creates too many combinations

Tags:

Imagine you have a number of overloaded methods that (before C++11) looked like this:

class MyClass {
public:
   void f(const MyBigType& a, int id);
   void f(const MyBigType& a, string name);
   void f(const MyBigType& a, int b, int c, int d);
   // ...
};

This function makes a copy of a (MyBigType), so I want to add an optimization by providing a version of f that moves a instead of copying it.

My problem is that now the number of f overloads will duplicate:

class MyClass {
public:
   void f(const MyBigType& a, int id);
   void f(const MyBigType& a, string name);
   void f(const MyBigType& a, int b, int c, int d);
   // ...
   void f(MyBigType&& a, int id);
   void f(MyBigType&& a, string name);
   void f(MyBigType&& a, int b, int c, int d);
   // ...
};

If I had more parameters that could be moved, it would be unpractical to provide all the overloads.

Has anyone dealt with this issue? Is there a good solution/pattern to solve this problem?

Thanks!

like image 985
alexc Avatar asked Jan 15 '15 18:01

alexc


People also ask

What is the purpose of an rvalue reference?

Rvalue references is a small technical extension to the C++ language. Rvalue references allow programmers to avoid logically unnecessary copying and to provide perfect forwarding functions. They are primarily meant to aid in the design of higer performance and more robust libraries.

How do you pass rvalue reference to a function?

If you want pass parameter as rvalue reference,use std::move() or just pass rvalue to your function.

Can a function return an rvalue?

Typically rvalues are temporary objects that exist on the stack as the result of a function call or other operation. Returning a value from a function will turn that value into an rvalue. Once you call return on an object, the name of the object does not exist anymore (it goes out of scope), so it becomes an rvalue.

Can rvalue be modified?

rvalue of User Defined Data type can be modified. But it can be modified in same expression using its own member functions only.


1 Answers

Herb Sutter talks about something similar in a cppcon talk

This can be done but probably shouldn't. You can get the effect out using universal references and templates, but you want to constrain the type to MyBigType and things that are implicitly convertible to MyBigType. With some tmp tricks, you can do this:

class MyClass {
  public:
    template <typename T>
    typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type
    f(T&& a, int id);
};

The only template parameter will match against the actual type of the parameter, the enable_if return type disallows incompatible types. I'll take it apart piece by piece

std::is_convertible<T, MyBigType>::value

This compile time expression will evaluate to true if T can be converted implicitly to a MyBigType. For example, if MyBigType were a std::string and T were a char* the expression would be true, but if T were an int it would be false.

typename std::enable_if<..., void>::type // where the ... is the above

this expression will result in void in the case that the is_convertible expression is true. When it's false, the expression will be malformed, so the template will be thrown out.

Inside the body of the function you'll need to use perfect forwarding, if you are planning on copy assigning or move assigning, the body would be something like

{
    this->a_ = std::forward<T>(a);
}

Here's a coliru live example with a using MyBigType = std::string. As Herb says, this function can't be virtual and must be implemented in the header. The error messages you get from calling with a wrong type will be pretty rough compared to the non-templated overloads.


Thanks to Barry's comment for this suggestion, to reduce repetition, it's probably a good idea to create a template alias for the SFINAE mechanism. If you declare in your class

template <typename T>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value, void>::type;

then you could reduce the declarations to

template <typename T>
EnableIfIsMyBigType<T>
f(T&& a, int id);

However, this assumes all of your overloads have a void return type. If the return type differs you could use a two-argument alias instead

template <typename T, typename R>
using EnableIfIsMyBigType = typename std::enable_if<std::is_convertible<T, MyBigType>::value,R>::type;

Then declare with the return type specified

template <typename T>
EnableIfIsMyBigType<T, void> // void is the return type
f(T&& a, int id);


The slightly slower option is to take the argument by value. If you do

class MyClass {
  public:
    void f(MyBigType a, int id) {
        this->a_ = std::move(a); // move assignment
    } 
};

In the case where f is passed an lvalue, it will copy construct a from its argument, then move assign it into this->a_. In the case that f is passed an rvalue, it will move construct a from the argument and then move assign. A live example of this behavior is here. Note that I use -fno-elide-constructors, without that flag, the rvalue cases elides the move construction and only the move assignment takes place.

If the object is expensive to move (std::array for example) this approach will be noticeably slower than the super-optimized first version. Also, consider watching this part of Herb's talk that Chris Drew links to in the comments to understand when it could be slower than using references. If you have a copy of Effective Modern C++ by Scott Meyers, he discusses the ups and downs in item 41.

like image 51
Ryan Haining Avatar answered Oct 28 '22 04:10

Ryan Haining