44

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?

  • 17
    use forwarding references ? – Piotr Skotnicki Apr 26 '16 at 14:51
  • And then you can stick with the last constructor (I think, right ?) – Unda Apr 26 '16 at 14:52
  • @Unda the last constructor takes rvalue references – Piotr Skotnicki Apr 26 '16 at 14:53
  • @PiotrSkotnicki Yes but that's the syntax you use along with `std::foward` isn't it ? – Unda Apr 26 '16 at 14:55
  • 2
    This question does not have an answer that is always right for all types that might be `Loss` and `Optimizer`. The best answer depends on details such as: Are both `Loss` and `Optimizer` expensive to copy but cheaply movable? Are the writers and maintainers of this code comfortable with constraining templates (`e.g. enable_if`)? The pass-by-value solution is sometimes the way to go. If you go with with the forwarding reference solution, I highly recommend properly constraining it. If only one of `Loss` and `Optimizer` is cheaply movable, a hybrid solution could be considered. – Howard Hinnant Apr 26 '16 at 15:26
  • @Unda not exactly : for these rvalue references to turn into forwarding references, `Loss` and `Optimizer` have to be deduced types. – Quentin Apr 26 '16 at 15:32
  • 4
    I think that the code in the question is not only complex, but essentially incorrect. Look at the initializer `_loss(loss)`. Even if `loss` is of type `Loss&&` then this initializer will still treat `loss` as an *lvalue*. This is important, if unintuitive. @Federico, were you under the impression that `_loss(loss)` would "move" from the `Loss&& loss`? In fact, it will be copied in. – Aaron McDaid Apr 26 '16 at 19:39
  • @AaronMcDaid no, I know it would get copied, actually I just copy pasted the first line and replaced the `const &` with `&&` for this example. My code is a little more complicated than this, and didn't wanted to fill the question with superfluos stuff, but thanks for participating! – Federico Allocati Apr 26 '16 at 20:53
  • 1
    are `_loss` and `_optimizer` values or references? – M.M Apr 27 '16 at 12:08
  • I am confused as to why you wrote 4 cases in the first place instead of just going `LinearClassifier(const Loss& loss, const Optimizer& optimizer)`. The cases with rvalue reference suggest you might want to move out of arguments... but then you didn't actually use `std::move` on the arguments – M.M Apr 27 '16 at 12:10
  • @M.M, I think the point you've made is the same as I've made. Basically, anything that can bind to `Loss&&` can also bind to `const Loss&` and therefore there is no point in writing a `Loss&&` overload unless you do something different in the initializers, different from the `const Loss&&`. I think the questioner understands this, and understands that this is just "toy" code in the question. I would prefer more realistic code - as this "toy" code is really confusing – Aaron McDaid Apr 27 '16 at 12:23
  • @AaronMcDaid, @M.M I have edited it now! In my case _loss and _optimizer are values, and in particular for some Optimizers, move is cheaper than copy, so if an rvalue is passed, I want to move, and only copy if i get an lvalue reference. I just put the `Loss&&` for completion, because at some point some `Loss` type may be expensive to copy. – Federico Allocati Apr 27 '16 at 13:54

4 Answers4

37

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.

Community
  • 1
  • 1
lisyarus
  • 15,025
  • 3
  • 43
  • 68
  • 3
    We have now two responses with *'this is [exactly the | the precise] use case for'* - two different methods, both of which make sense to me. Could anyone clarify whether both are good or why we shouldn't even consider the other, or whether they are indeed interchangable? – peterchen Apr 26 '16 at 14:58
  • @peterchen My method is a bit cumbersome and far less readable, but avoids extra moves. It should be preffered if this extra move begins to be a problem. – lisyarus Apr 26 '16 at 15:00
  • 1
    @FedericoAllocati Yes, it does. – lisyarus Apr 26 '16 at 15:01
  • 1
    Not all moves are cheap... some moves are copies. – Barry Apr 26 '16 at 15:04
  • @Barry Sure, and this is precisely the case where we need perfect forwarding. – lisyarus Apr 26 '16 at 15:04
  • 1
    Templated constructors are not always desirable, which can make "copy and move" more attractive. – Ilya Popov Apr 26 '16 at 15:09
  • 7
    This design is a decent direction to head, but as it stands, has a defect that might be serious: `std::is_constructible::value` is `true` (and you can sub in anything you want for `int`). If you don't care, fine. But correct SFINAE is becoming more and more important. To fix this, you either go with the by-value solution from the other answer, or you constrain `L` and `O` such that they will only instantiate for `Loss` and `Optimizer`, and this answer does not (yet) explain how to do that. – Howard Hinnant Apr 26 '16 at 15:15
  • 1
    @HowardHinnant [As requested](http://stackoverflow.com/a/36870163/2069064). Though it'd be cool if the by-value solution could be optimal... Certainly it's a lot easier to write and understand. – Barry Apr 26 '16 at 16:15
  • How can I provide that lvalue object will not be changed in such constructor with std::forward? Here we have L&& loss, if we give lvalue - we will have L& loss, and it can be changed. The good practice everytime was using const SomeType&, but here we can’t just write const L&& loss, because we want to have move ability. What is the solution? – I.S.M. Nov 26 '17 at 12:02
31

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.

Community
  • 1
  • 1
Ilya Popov
  • 3,765
  • 1
  • 17
  • 30
  • _"The perfect forwarding solution is more efficient."_ Realistically, it _might_ be more efficient. – edmz Apr 26 '16 at 15:13
  • I didn't understood the part of " because it will every argument types then not constrained with SFINAE and lead to hard errors inside the constructor if arguments are not compatible". The header file placement is not a problem, because this is a header only library :) – Federico Allocati Apr 26 '16 at 15:14
  • The constructor shown by @lisyarus will fit any call with two arguments regardless of their types. This leads to several consequences: you cannot have any other constructor with two arguments, and if some other code tries to do any SFINAE tricks using your constructor, it won't work (because the constructor will accept any types and then produce an error inside the constructor body). (See Howard Hinnant's comment for an example). – Ilya Popov Apr 26 '16 at 15:17
  • "templated constructors are not SFINAE-friendly" That's just tautological. Non-SFINAE-friendly constructor templates aren't SFINAE-friendly... but SFINAE-friendly ones are... – Barry Apr 26 '16 at 16:08
  • @Barry thats why I said "then not constrained". Of course, they are SFINAE-friendly if constrained properly. – Ilya Popov Apr 26 '16 at 16:17
30

For the sake of completeness, the optimal 2-argument constructor would take two forwarding references and use SFINAE to ensure that they're the correct types. We can introduce the following alias:

template <class T, class U>
using decays_to = std::is_convertible<std::decay_t<T>*, U*>;

And then:

template <class L, class O,
          class = std::enable_if_t<decays_to<L, Loss>::value &&
                                   decays_to<O, Optimizer>::value>>
LinearClassifier(L&& loss, O&& optimizer)
: _loss(std::forward<L>(loss))
, _optimizer(std::forward<O>(optimizer))
{ }

This ensures that we only accept arguments that are of type Loss and Optimizer (or are derived from them). Unfortunately, it is quite a mouthful to write and is very distracting from the original intent. This is pretty difficult to get right - but if performance matters, then it matters, and this is really the only way to go.

But if it doesn't matter, and if Loss and Optimizer are cheap to move (or, better still, performance for this constructor is completely irrelevant), prefer Ilya Popov's solution:

LinearClassifier(Loss loss, Optimizer optimizer)
: _loss(std::move(loss))
, _optimizer(std::move(optimizer))
{ }
Community
  • 1
  • 1
Barry
  • 286,269
  • 29
  • 621
  • 977
  • 1
    Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (https://www.youtube.com/watch?v=xnqTKD8uD64) :-). What about using `std::is_convertible` for the constraint? This would (for example) allow a `const char*` `L` to construct a `std::string` `Loss`. And would also allow your Derived -> Base example. – Howard Hinnant Apr 26 '16 at 16:30
  • @HowardHinnant Could use `std::is_constructible` too. Just wanted to be as strict as possible for genericity. But yeah, hard to pick... What am I looking for in that video? :) – Barry Apr 26 '16 at 16:47
  • Just an hour before that talk, and on my way out of town, Herb asked me what the constraint should be for a problem like this. Toward the end of the talk (1:15:00 in?). I over-thought it and got it wrong. There's nothing like testing! :-) – Howard Hinnant Apr 26 '16 at 16:59
  • @Howard I think the constraint is fine! So you're requiring the user to be explicit - that's hardly infamous-worthy – Barry Apr 26 '16 at 17:39
  • here's some [syntactic sugar](http://melpon.org/wandbox/permlink/3JTRY8HSOrNCoYYE) using fold-expressions that allows to pass the -IMO pretty readible- expression `forward_compatible_v, std::pair>` to the `enable_if_t` constraint. – TemplateRex Apr 27 '16 at 09:43
  • How can I provide that lvalue object will not be changed in such constructor with std::forward? Here we have L&& loss, if we give lvalue - we will have L& loss, and it can be changed. The good practice everytime was using const SomeType&, but here we can’t just write const L&& loss, because we want to have move ability. What is the solution? – I.S.M. Nov 26 '17 at 12:01
16

How far down the rabbit hole do you want to go?

I'm aware of 4 decent ways to approach this problem. You should generally use the earlier ones if you match their preconditions, as each later one increases significantly in complexity.


For the most part, either move is so cheap doing it twice is free, or move is copy.

If move is copy, and copy is non-free, take the parameter by const&. If not, take it by value.

This will behave basically optimally, and makes your code far easier to understand.

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

for a cheap-to-move Loss and move-is-copy optimizer.

This does 1 extra move over the "optimal" perfect forwarding below (note: perfect forwarding is not optimal) per value parameter in all cases. So long as move is cheap, this is the best solution, because it generates clean error messages, allows {} based construction, and is far easier to read than any of the other solutions.

Consider using this solution.


If move is cheaper than copy yet non-free, one approach is perfect forwarding based: Either:

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

Or the more complex and more overload-friendly:

template<class L, class O,
  std::enable_if_t<
    std::is_same<std::decay_t<L>, Loss>{}
    && std::is_same<std::decay_t<O>, Optimizer>{}
  , int> * = nullptr
>
LinearClassifier(L&& loss, O&& optimizer)
  : _loss(std::forward<L>(loss))
  , _optimizer(std::forward<O>(optimizer))
{}

this costs you the ability to do {} based construction of your arguments. Also, up to exponential number of constructors can be generated by the above code if they are called (hopefully they will be inlined).

You can drop the std::enable_if_t clause at the cost of SFINAE failure; basically, the wrong overload of your constructor can be picked if you aren't careful with that std::enable_if_t clause. If you have constructor overloads with the same number of arguments, or care about early-failure, then you want the std::enable_if_t one. Otherwise, use the simpler one.

This solution is usually considered "most optimal". It is accepably optimal, but it is not most optimal.


The next step is to use emplace construction with tuples.

private:
template<std::size_t...LIs, std::size_t...OIs, class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
  std::index_sequence<LIs...>, std::tuple<Ls...>&& ls,
  std::index_sequence<OIs...>, std::tuple<Os...>&& os
)
  : _loss(std::get<LIs>(std::move(ls))...)
  , _optimizer(std::get<OIs>(std::move(os))...)
{}
public:
template<class...Ls, class...Os>
LinearClassifier(std::piecewise_construct_t,
  std::tuple<Ls...> ls,
  std::tuple<Os...> os
):
  LinearClassifier(std::piecewise_construct_t{},
    std::index_sequence_for<Ls...>{}, std::move(ls),
    std::index_sequence_for<Os...>{}, std::move(os)
  )
{}

where we defer construction until inside the LinearClassifier. This allows you to have non-copy/moveable objects in your object, and is arguably maximally efficient.

To see how this works, example now piecewise_construct works with std::pair. You pass piecewise construct first, then forward_as_tuple the arguments to construct each element afterwards (including a copy or move ctor).

By directly constructing objects, we can eliminate a move or a copy per object compared to the perfect-forwarding solution above. It also lets you forward a copy or a move if required.


A final cute technique is to type-erase construction. Practically, this requires something like std::experimental::optional<T> to be available, and might make the class a bit larger.

This is not faster than the piecewise construction one. It does abstract the work that the emplace construction one does, making it simpler on a per-use basis, and it permits you to split ctor body from the header file. But there is a small amount of overhead, in both runtime and space.

There is a bunch of boilerplate you need to start with. This generates a template class that represents the concept of "constructing an object, later, at a place someone else will tell me."

struct delayed_emplace_t {};
template<class T>
struct delayed_construct {
  std::function< void(std::experimental::optional<T>&) > ctor;
  delayed_construct(delayed_construct const&)=delete; // class is single-use
  delayed_construct(delayed_construct &&)=default;
  delayed_construct():
    ctor([](auto&op){op.emplace();})
  {}
  template<class T, class...Ts,
    std::enable_if_t<
      sizeof...(Ts)!=0
      || !std::is_same<std::decay_t<T>, delayed_construct>{}
    ,int>* = nullptr
  >
  delayed_construct(T&&t, Ts&&...ts):
    delayed_construct( delayed_emplace_t{}, std::forward<T>(t), std::forward<Ts>(ts)... )
  {}
  template<class T, class...Ts>
  delayed_construct(delayed_emplace_t, T&&t, Ts&&...ts):
    ctor([tup = std::forward_as_tuple(std::forward<T>(t), std::forward<Ts>(ts)...)]( auto& op ) mutable {
      ctor_helper(op, std::make_index_sequence<sizeof...(Ts)+1>{}, std::move(tup));
    })
  template<std::size_t...Is, class...Ts>
  static void ctor_helper(std::experimental::optional<T>& op, std::index_sequence<Is...>, std::tuple<Ts...>&& tup) {
    op.emplace( std::get<Is>(std::move(tup))... );
  }
  void operator()(std::experimental::optional<T>& target) {
    ctor(target);
    ctor = {};
  }
  explicit operator bool() const { return !!ctor; }
};

where we type-erase the action of constructing an optional from arbitrary arguments.

LinearClassifier( delayed_construct<Loss> loss, delayed_construct<Optimizer> optimizer ) {
  loss(_loss);
  optimizer(_optimizer);
}

where _loss are std::experimental::optional<Loss>. To remove the optionality of _loss you have to use std::aligned_storage_t<sizeof(Loss), alignof(Loss)> and be very careful about writing a ctor to handle exceptions and manually destroy things etc. It is a headache.

Some nice things about this last pattern is that the body of the ctor can move out of the header, and at most a linear amount of code is generated instead of an exponential amount of template constructors.

This solution is marginally less efficient than the placement construct version, as not all compilers will be able to inline the std::function use. But it also permits storing non-movable objects.

Code not tested, so there are probably typos.


In with guaranteed elision, the optional part of the delayed ctor becomes obsolete. Any function returning a T is all you need for a delayed ctor of T.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
  • I just love the way the code explodes with each iteration of "optimality" ;-) Somehow C++ seems to have left all the simplicity of C behind... – cmaster - reinstate monica Apr 26 '16 at 19:31
  • 3
    I'm not sure if I should be disgusted or in awe. – isanae Apr 26 '16 at 19:33
  • 1
    @cmaster To some extent; but doing the same kind of operations in C would be bulkier and completely unmaintainable and next to impossible to do without repeating every time you want to use it. The `delayed_construct` (which is the most insane) actually has a really short "per use" body (same length as first solution!), and what it does would be a real headache in C. You'd best give up long before you reached what is actually going on in that one, and no chance of doing it generically. In C++, I write the mess once (and the mess is *shorter* than the C equivalent), and can reuse it. – Yakk - Adam Nevraumont Apr 26 '16 at 23:28
  • 1
    @cmaster Now, I probably wouldn't use it; I'd argue for #1 barring extreme circumstances. And #1 is already ridiculously shorter than the equivalent C implementation. The C solution might be as short as #1, but that is because it *usually* wouldn't do the same amount of corner-case optimization stuff as even #1 does "under the hood". – Yakk - Adam Nevraumont Apr 26 '16 at 23:30
  • How can I provide that lvalue object will not be changed in such constructor with std::forward? Here we have L&& loss, if we give lvalue - we will have L& loss, and it can be changed. The good practice everytime was using const SomeType&, but here we can’t just write const L&& loss, because we want to have move ability. What is the solution? – I.S.M. Nov 26 '17 at 12:01
  • @i.s.m. It can be changed. Taking things by `const&` does not prevent it from being changed, `const_cast` away `const` is legal C++. And rvalues are changed by being moved-from. Or is your concern with screwing up the body of the function? You could write a move-or-copy forwarder if this becomes a concern of yours. – Yakk - Adam Nevraumont Nov 26 '17 at 13:34
  • I’m a bit confused, that we had smth like that: LinearClassifier(Loss&& loss, const Optimizer& optimizer) - without forward and co., but with them we got LinearClassifier(Loss&& loss, Optimizer& optimizer) - without const. Here we can change lvalue optimizer without any const_cast. I think it’s more dangerous, than const Optimizer&. Therefore I’m trying to find a solution, that will add const, if we want this and if initial type was lvalue only. Is it correct to do so or not? – I.S.M. Nov 26 '17 at 13:42
  • @i.s.m. sorry, too many tyops and abbrev.; I do not understand your concern. I advise you that if you have a practical question to use "ask a question" button above. – Yakk - Adam Nevraumont Nov 26 '17 at 14:05