Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Avoid exponential grow of const references and rvalue references in constructor

I am coding some templated classes for a machine learning library, and I'm facing this issue a lot of times. I'm using mostly the policy pattern, where classes receive as template argument policies for different functionalities, for example:

template <class Loss, class Optimizer> class LinearClassifier { ... } 

The problem is with the constructors. As the amount of policies (template parameters) grows, the combinations of const references and rvalue references grow exponentially. In the previous example:

LinearClassifier(const Loss& loss, const Optimizer& optimizer) : _loss(loss), _optimizer(optimizer) {}  LinearClassifier(Loss&& loss, const Optimizer& optimizer) : _loss(std::move(loss)), _optimizer(optimizer) {}  LinearClassifier(const Loss& loss, Optimizer&& optimizer) : _loss(loss), _optimizer(std::move(optimizer)) {}  LinearClassifier(Loss&& loss, Optimizer&& optimizer) : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {} 

Is there some way to avoid this?

like image 231
Federico Allocati Avatar asked Apr 26 '16 14:04

Federico Allocati


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.

Can lvalue bind to rvalue reference?

An lvalue reference can bind to an lvalue, but not to an rvalue.

Can R values be const?

If you could pass an r-value by (non-const) reference, you could also be able to assign to it from inside the function. Therefore the rule that if r-values are to be passed by reference, this has to be a const reference.

What is double ampersand C++?

It denotes an rvalue reference. Rvalue references will only bind to temporary objects, unless explicitly generated otherwise. They are used to make objects much more efficient under certain circumstances, and to provide a facility known as perfect forwarding, which greatly simplifies template code.


2 Answers

Actually, this is the precise reason why perfect forwarding was introduced. Rewrite the constructor as

template <typename L, typename O> LinearClassifier(L && loss, O && optimizer)     : _loss(std::forward<L>(loss))     , _optimizer(std::forward<O>(optimizer)) {} 

But it will probably be much simpler to do what Ilya Popov suggests in his answer. To be honest, I usually do it this way, since moves are intended to be cheap and one more move does not change things dramatically.

As Howard Hinnant has told, my method can be SFINAE-unfriendly, since now LinearClassifier accepts any pair of types in constructor. Barry's answer shows how to deal with it.

like image 56
lisyarus Avatar answered Sep 20 '22 17:09

lisyarus


This is exactly the use case for "pass by value and move" technique. Although slighly less efficient than lvalue/rvalue overloads, it not too bad (one extra move) and saves you the hassle.

LinearClassifier(Loss loss, Optimizer optimizer)      : _loss(std::move(loss)), _optimizer(std::move(optimizer)) {} 

In the case of lvalue argument, there will be one copy and one move, in the case of rvalue argument, there will be two moves (provided that you classes Loss and Optimizer implement move constructors).

Update: In general, perfect forwarding solution is more efficient. On the other hand, this solution avoids templated constructors which are not always desirable, because it will accept arguments of any type when not constrained with SFINAE and lead to hard errors inside the constructor if arguments are not compatible. In other words, unconstrained templated constructors are not SFINAE-friendly. See Barry's answer for a constrained template constructor which avoids this problem.

Another potential problem of a templated constructor is the need to place it in a header file.

Update 2: Herb Sutter talks about this problem in his CppCon 2014 talk "Back to the Basics" starting at 1:03:48. He discusses pass by value first, then overloading on rvalue-ref, then perfect forwarding at 1:15:22 including constraining. And finally he talks about constructors as the only good use case for passing by value at 1:25:50.

like image 32
Ilya Popov Avatar answered Sep 21 '22 17:09

Ilya Popov