Avoid exponential grow of const references and rvalue references in constructor The 2019 Stack Overflow Developer Survey Results Are In Unicorn Meta Zoo #1: Why another podcast? Announcing the arrival of Valued Associate #679: Cesar Manara The Ask Question Wizard is Live! Data science time! April 2019 and salary with experiencec++ receive const lvalue and rvalue reference parameter without overloadmove/copy constructors combination of multiple parametersTo support move semantics, should function parameters be taken by unique_ptr, by value, or by rvalue?Some clarification on rvalue referencesRvalue reference usage within initialization listsHow to make template rvalue reference parameter ONLY bind to rvalue reference?rvalue references and constructor argumentsWould you ever mark a C++ RValue reference parameter as constGeneric copy-constructor with rvalue-reference membersPassing rvalue reference to const lvalue reference paremeterHow std::move can work with copy-constructor that takes non-const reference?What do you call a constructor with an rvalue reference parameter?invalid initialization of non-const reference from an rvalue
How do you keep chess fun when your opponent constantly beats you?
How did the audience guess the pentatonic scale in Bobby McFerrin's presentation?
Could an empire control the whole planet with today's comunication methods?
Sub-subscripts in strings cause different spacings than subscripts
Is every episode of "Where are my Pants?" identical?
Python - Fishing Simulator
How to determine omitted units in a publication
My body leaves; my core can stay
Do warforged have souls?
Keeping a retro style to sci-fi spaceships?
Huge performance difference of the command find with and without using %M option to show permissions
should truth entail possible truth
Accepted by European university, rejected by all American ones I applied to? Possible reasons?
Is 'stolen' appropriate word?
Word for: a synonym with a positive connotation?
Simulating Exploding Dice
How to politely respond to generic emails requesting a PhD/job in my lab? Without wasting too much time
Why did Peik Lin say, "I'm not an animal"?
Circular reasoning in L'Hopital's rule
What to do when moving next to a bird sanctuary with a loosely-domesticated cat?
Can we generate random numbers using irrational numbers like π and e?
"... to apply for a visa" or "... and applied for a visa"?
Do ℕ, mathbbN, BbbN, symbbN effectively differ, and is there a "canonical" specification of the naturals?
What other Star Trek series did the main TNG cast show up in?
Avoid exponential grow of const references and rvalue references in constructor
The 2019 Stack Overflow Developer Survey Results Are In
Unicorn Meta Zoo #1: Why another podcast?
Announcing the arrival of Valued Associate #679: Cesar Manara
The Ask Question Wizard is Live!
Data science time! April 2019 and salary with experiencec++ receive const lvalue and rvalue reference parameter without overloadmove/copy constructors combination of multiple parametersTo support move semantics, should function parameters be taken by unique_ptr, by value, or by rvalue?Some clarification on rvalue referencesRvalue reference usage within initialization listsHow to make template rvalue reference parameter ONLY bind to rvalue reference?rvalue references and constructor argumentsWould you ever mark a C++ RValue reference parameter as constGeneric copy-constructor with rvalue-reference membersPassing rvalue reference to const lvalue reference paremeterHow std::move can work with copy-constructor that takes non-const reference?What do you call a constructor with an rvalue reference parameter?invalid initialization of non-const reference from an rvalue
.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty,.everyoneloves__bot-mid-leaderboard:empty height:90px;width:728px;box-sizing:border-box;
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?
c++ c++11 rvalue-reference const-reference
|
show 8 more comments
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?
c++ c++11 rvalue-reference const-reference
17
use forwarding references ?
– Piotr Skotnicki
Apr 26 '16 at 14:51
1
Just copy and move.
– edmz
Apr 26 '16 at 14:54
2
This question does not have an answer that is always right for all types that might beLoss
andOptimizer
. The best answer depends on details such as: Are bothLoss
andOptimizer
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 ofLoss
andOptimizer
is cheaply movable, a hybrid solution could be considered.
– Howard Hinnant
Apr 26 '16 at 15:26
4
I think that the code in the question is not only complex, but essentially incorrect. Look at the initializer_loss(loss)
. Even ifloss
is of typeLoss&&
then this initializer will still treatloss
as an lvalue. This is important, if unintuitive. @Federico, were you under the impression that_loss(loss)
would "move" from theLoss&& loss
? In fact, it will be copied in.
– Aaron McDaid
Apr 26 '16 at 19:39
1
are_loss
and_optimizer
values or references?
– M.M
Apr 27 '16 at 12:08
|
show 8 more comments
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?
c++ c++11 rvalue-reference const-reference
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?
c++ c++11 rvalue-reference const-reference
c++ c++11 rvalue-reference const-reference
edited Apr 27 '16 at 13:40
Federico Allocati
asked Apr 26 '16 at 14:49
Federico AllocatiFederico Allocati
30738
30738
17
use forwarding references ?
– Piotr Skotnicki
Apr 26 '16 at 14:51
1
Just copy and move.
– edmz
Apr 26 '16 at 14:54
2
This question does not have an answer that is always right for all types that might beLoss
andOptimizer
. The best answer depends on details such as: Are bothLoss
andOptimizer
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 ofLoss
andOptimizer
is cheaply movable, a hybrid solution could be considered.
– Howard Hinnant
Apr 26 '16 at 15:26
4
I think that the code in the question is not only complex, but essentially incorrect. Look at the initializer_loss(loss)
. Even ifloss
is of typeLoss&&
then this initializer will still treatloss
as an lvalue. This is important, if unintuitive. @Federico, were you under the impression that_loss(loss)
would "move" from theLoss&& loss
? In fact, it will be copied in.
– Aaron McDaid
Apr 26 '16 at 19:39
1
are_loss
and_optimizer
values or references?
– M.M
Apr 27 '16 at 12:08
|
show 8 more comments
17
use forwarding references ?
– Piotr Skotnicki
Apr 26 '16 at 14:51
1
Just copy and move.
– edmz
Apr 26 '16 at 14:54
2
This question does not have an answer that is always right for all types that might beLoss
andOptimizer
. The best answer depends on details such as: Are bothLoss
andOptimizer
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 ofLoss
andOptimizer
is cheaply movable, a hybrid solution could be considered.
– Howard Hinnant
Apr 26 '16 at 15:26
4
I think that the code in the question is not only complex, but essentially incorrect. Look at the initializer_loss(loss)
. Even ifloss
is of typeLoss&&
then this initializer will still treatloss
as an lvalue. This is important, if unintuitive. @Federico, were you under the impression that_loss(loss)
would "move" from theLoss&& loss
? In fact, it will be copied in.
– Aaron McDaid
Apr 26 '16 at 19:39
1
are_loss
and_optimizer
values or references?
– M.M
Apr 27 '16 at 12:08
17
17
use forwarding references ?
– Piotr Skotnicki
Apr 26 '16 at 14:51
use forwarding references ?
– Piotr Skotnicki
Apr 26 '16 at 14:51
1
1
Just copy and move.
– edmz
Apr 26 '16 at 14:54
Just copy and move.
– edmz
Apr 26 '16 at 14:54
2
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
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
4
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
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
1
1
are
_loss
and _optimizer
values or references?– M.M
Apr 27 '16 at 12:08
are
_loss
and _optimizer
values or references?– M.M
Apr 27 '16 at 12:08
|
show 8 more comments
4 Answers
4
active
oldest
votes
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.
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
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
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<LinearClassifier, int, int>::value
istrue
(and you can sub in anything you want forint
). 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 constrainL
andO
such that they will only instantiate forLoss
andOptimizer
, and this answer does not (yet) explain how to do that.
– Howard Hinnant
Apr 26 '16 at 15:15
|
show 5 more comments
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.
"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
add a comment |
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))
1
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about usingstd::is_convertible<L, Loss>
for the constraint? This would (for example) allow aconst char*
L
to construct astd::string
Loss
. And would also allow your Derived -> Base example.
– Howard Hinnant
Apr 26 '16 at 16:30
@HowardHinnant Could usestd::is_constructible<Loss, L&&>
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 using fold-expressions that allows to pass the -IMO pretty readible- expressionforward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to theenable_if_t
constraint.
– TemplateRex
Apr 27 '16 at 9:43
|
show 1 more comment
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::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 c++17 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
.
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
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
@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. Thedelayed_construct<T>
(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
@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
|
show 3 more comments
Your Answer
StackExchange.ifUsing("editor", function ()
StackExchange.using("externalEditor", function ()
StackExchange.using("snippets", function ()
StackExchange.snippets.init();
);
);
, "code-snippets");
StackExchange.ready(function()
var channelOptions =
tags: "".split(" "),
id: "1"
;
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function()
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled)
StackExchange.using("snippets", function()
createEditor();
);
else
createEditor();
);
function createEditor()
StackExchange.prepareEditor(
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader:
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
,
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
);
);
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f36868442%2favoid-exponential-grow-of-const-references-and-rvalue-references-in-constructor%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
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.
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
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
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<LinearClassifier, int, int>::value
istrue
(and you can sub in anything you want forint
). 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 constrainL
andO
such that they will only instantiate forLoss
andOptimizer
, and this answer does not (yet) explain how to do that.
– Howard Hinnant
Apr 26 '16 at 15:15
|
show 5 more comments
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.
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
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
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<LinearClassifier, int, int>::value
istrue
(and you can sub in anything you want forint
). 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 constrainL
andO
such that they will only instantiate forLoss
andOptimizer
, and this answer does not (yet) explain how to do that.
– Howard Hinnant
Apr 26 '16 at 15:15
|
show 5 more comments
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.
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.
edited May 23 '17 at 12:34
Community♦
11
11
answered Apr 26 '16 at 14:53
lisyaruslisyarus
10.5k22952
10.5k22952
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
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
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<LinearClassifier, int, int>::value
istrue
(and you can sub in anything you want forint
). 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 constrainL
andO
such that they will only instantiate forLoss
andOptimizer
, and this answer does not (yet) explain how to do that.
– Howard Hinnant
Apr 26 '16 at 15:15
|
show 5 more comments
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
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
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<LinearClassifier, int, int>::value
istrue
(and you can sub in anything you want forint
). 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 constrainL
andO
such that they will only instantiate forLoss
andOptimizer
, and this answer does not (yet) explain how to do that.
– Howard Hinnant
Apr 26 '16 at 15:15
3
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
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
1
1
@FedericoAllocati Yes, it does.
– lisyarus
Apr 26 '16 at 15:01
@FedericoAllocati Yes, it does.
– lisyarus
Apr 26 '16 at 15:01
1
1
Not all moves are cheap... some moves are copies.
– Barry
Apr 26 '16 at 15:04
Not all moves are cheap... some moves are copies.
– Barry
Apr 26 '16 at 15:04
1
1
Templated constructors are not always desirable, which can make "copy and move" more attractive.
– Ilya Popov
Apr 26 '16 at 15:09
Templated constructors are not always desirable, which can make "copy and move" more attractive.
– Ilya Popov
Apr 26 '16 at 15:09
7
7
This design is a decent direction to head, but as it stands, has a defect that might be serious:
std::is_constructible<LinearClassifier, int, int>::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
This design is a decent direction to head, but as it stands, has a defect that might be serious:
std::is_constructible<LinearClassifier, int, int>::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
|
show 5 more comments
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.
"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
add a comment |
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.
"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
add a comment |
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.
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.
edited May 23 '17 at 11:54
Community♦
11
11
answered Apr 26 '16 at 14:54
Ilya PopovIlya Popov
2,7791927
2,7791927
"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
add a comment |
"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
"The perfect forwarding solution is more efficient." Realistically, it might be more efficient.
– edmz
Apr 26 '16 at 15:13
"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
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
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
"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
@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
add a comment |
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))
1
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about usingstd::is_convertible<L, Loss>
for the constraint? This would (for example) allow aconst char*
L
to construct astd::string
Loss
. And would also allow your Derived -> Base example.
– Howard Hinnant
Apr 26 '16 at 16:30
@HowardHinnant Could usestd::is_constructible<Loss, L&&>
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 using fold-expressions that allows to pass the -IMO pretty readible- expressionforward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to theenable_if_t
constraint.
– TemplateRex
Apr 27 '16 at 9:43
|
show 1 more comment
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))
1
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about usingstd::is_convertible<L, Loss>
for the constraint? This would (for example) allow aconst char*
L
to construct astd::string
Loss
. And would also allow your Derived -> Base example.
– Howard Hinnant
Apr 26 '16 at 16:30
@HowardHinnant Could usestd::is_constructible<Loss, L&&>
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 using fold-expressions that allows to pass the -IMO pretty readible- expressionforward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to theenable_if_t
constraint.
– TemplateRex
Apr 27 '16 at 9:43
|
show 1 more comment
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))
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))
edited May 23 '17 at 12:34
Community♦
11
11
answered Apr 26 '16 at 16:03
BarryBarry
187k21329606
187k21329606
1
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about usingstd::is_convertible<L, Loss>
for the constraint? This would (for example) allow aconst char*
L
to construct astd::string
Loss
. And would also allow your Derived -> Base example.
– Howard Hinnant
Apr 26 '16 at 16:30
@HowardHinnant Could usestd::is_constructible<Loss, L&&>
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 using fold-expressions that allows to pass the -IMO pretty readible- expressionforward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to theenable_if_t
constraint.
– TemplateRex
Apr 27 '16 at 9:43
|
show 1 more comment
1
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about usingstd::is_convertible<L, Loss>
for the constraint? This would (for example) allow aconst char*
L
to construct astd::string
Loss
. And would also allow your Derived -> Base example.
– Howard Hinnant
Apr 26 '16 at 16:30
@HowardHinnant Could usestd::is_constructible<Loss, L&&>
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 using fold-expressions that allows to pass the -IMO pretty readible- expressionforward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to theenable_if_t
constraint.
– TemplateRex
Apr 27 '16 at 9:43
1
1
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about using
std::is_convertible<L, Loss>
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
Interesting choice of constraint. I agree, this is tricky to get right, and I am on the hook for infamously getting it wrong (youtube.com/watch?v=xnqTKD8uD64) :-). What about using
std::is_convertible<L, Loss>
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<Loss, L&&>
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
@HowardHinnant Could use
std::is_constructible<Loss, L&&>
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
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
@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 using fold-expressions that allows to pass the -IMO pretty readible- expression
forward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to the enable_if_t
constraint.– TemplateRex
Apr 27 '16 at 9:43
here's some syntactic sugar using fold-expressions that allows to pass the -IMO pretty readible- expression
forward_compatible_v<std::pair<L, Loss>, std::pair<O, Optimizer>>
to the enable_if_t
constraint.– TemplateRex
Apr 27 '16 at 9:43
|
show 1 more comment
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::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 c++17 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
.
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
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
@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. Thedelayed_construct<T>
(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
@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
|
show 3 more comments
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::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 c++17 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
.
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
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
@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. Thedelayed_construct<T>
(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
@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
|
show 3 more comments
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::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 c++17 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
.
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::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 c++17 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
.
edited Feb 20 at 1:16
answered Apr 26 '16 at 17:18
Yakk - Adam NevraumontYakk - Adam Nevraumont
189k21199385
189k21199385
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
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
@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. Thedelayed_construct<T>
(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
@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
|
show 3 more comments
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
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
@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. Thedelayed_construct<T>
(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
@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 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
Apr 26 '16 at 19:31
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
Apr 26 '16 at 19:31
3
3
I'm not sure if I should be disgusted or in awe.
– isanae
Apr 26 '16 at 19:33
I'm not sure if I should be disgusted or in awe.
– isanae
Apr 26 '16 at 19:33
@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<T>
(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
@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<T>
(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
@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
@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
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
|
show 3 more comments
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function ()
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f36868442%2favoid-exponential-grow-of-const-references-and-rvalue-references-in-constructor%23new-answer', 'question_page');
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function ()
StackExchange.helpers.onClickDraftSave('#login-link');
);
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
17
use forwarding references ?
– Piotr Skotnicki
Apr 26 '16 at 14:51
1
Just copy and move.
– edmz
Apr 26 '16 at 14:54
2
This question does not have an answer that is always right for all types that might be
Loss
andOptimizer
. The best answer depends on details such as: Are bothLoss
andOptimizer
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 ofLoss
andOptimizer
is cheaply movable, a hybrid solution could be considered.– Howard Hinnant
Apr 26 '16 at 15:26
4
I think that the code in the question is not only complex, but essentially incorrect. Look at the initializer
_loss(loss)
. Even ifloss
is of typeLoss&&
then this initializer will still treatloss
as an lvalue. This is important, if unintuitive. @Federico, were you under the impression that_loss(loss)
would "move" from theLoss&& loss
? In fact, it will be copied in.– Aaron McDaid
Apr 26 '16 at 19:39
1
are
_loss
and_optimizer
values or references?– M.M
Apr 27 '16 at 12:08