1

For two arbitrary objects of type T and U that are composed into a class like so

template <class T, class U>
struct Comp
{
    T t_m; 
    U u_m; 
}; 

what would be the optimum (in terms of minimizing copy operations) way to construct them out of (available) temporaries ?

I have considered "moving" them into my class

Comp(T&& t, U&& u)
    : t_m(std::move(t))
    , u_m(std::move(u))
{ }

but I don't know how well their move constructors behave or if they have any whatsoever.

Since it seems that my class can be an aggregate I was wondering whether removing the constructor and allowing aggregate initialization would be a better solution, i.e. writing code like this:

Comp{ get_temporary_T(), get_temporary_U() }; 

or if there's an advantage in using direct initialization.

PS

In place construction (using placement new operator) is not the solution I'm looking for.

PS 2

I imagine std::tuple uses such an optimum method since make_tuple is shown to utilize temporaries by calling the tuple constructor :

auto t = std::make_tuple(10, "Test", 3.14, std::ref(n), n);

could alternatively someone elaborate on how this is done ?

Lorah Attkins
  • 5,331
  • 3
  • 29
  • 63
  • too broad ? "what would be the optimum (in terms of minimizing copy operations) way to construct them out of (available) temporaries ?" is too broad ? – Lorah Attkins Mar 26 '16 at 01:10
  • Why not test the simplest of code with optimizations turned on first, before embarking on trying to outdo the compiler's optimizer? – PaulMcKenzie Mar 26 '16 at 01:15
  • @PaulMcKenzie Could you help me on how to do this? Do you mean inspect the machine code, benchmark for speed, instrument my code? – Lorah Attkins Mar 26 '16 at 01:20
  • 4
    "*I imagine std::tuple uses such an optimum method since make_tuple is shown to utilize temporaries by calling the tuple constructor:*" It's not clear what you're referring to here, or how what `make_tuple` does is in *any way* more optimal than your first option. – Nicol Bolas Mar 26 '16 at 01:20
  • @LorahAttkins - Compiler optimizers can do amazing things these days. Code you think is inefficient turns out it isn't, all due to compilers eliminating unnecessary copies. This means you build your program with optimizations turned on, and see if all of your fears are indeed there. – PaulMcKenzie Mar 26 '16 at 01:23
  • @NicolBolas I have a class and it has two members. I also have two temporaries of the same type as these members. How should I construct my class so that I'll make the best use of the objects I already have? I've been reading on direct, copy, list, aggregate and list initialization and I still can't figure this out (it seems that copying is never out of the table as far as standard guarantees go). If you can elaborate on an **answer** please, please, please do so. – Lorah Attkins Mar 26 '16 at 01:29
  • 2
    As a side note, you can actually check if your types have a move constructor: http://en.cppreference.com/w/cpp/types/is_move_constructible – bku_drytt Mar 26 '16 at 01:45
  • @LorahAttkins "*it seems that copying is never out of the table as far as standard guarantees go*" I guess that's the part I don't understand; why do you think it matters if copying is on or off the table? You talk about "utilize temporaries" but copying "utilize temporaries" just as much as moving. I get the impression that there's something about the concept of movement in C++ that you're not understanding. – Nicol Bolas Mar 26 '16 at 02:19
  • @NicolBolas Oh there's plenty about the concept of movement that eludes me. By "utilize" I mean "use the objects themselves" and by copying not being on the table I mean this (utilization) should happen not by relying on some compiler dependent optimization but on standard guarantees. – Lorah Attkins Mar 26 '16 at 08:54

2 Answers2

2

This is already optimal:

Comp(T&& t, U&& u)
: t_m(std::move(t))
, u_m(std::move(u))
{ }

If T has a move constructor, this'll be a move. If it doesn't, this'll be a copy - but then there's no way around making a copy somewhere. And it isn't copyable, then the whole question is moot.

Of course this only works for rvalues, so you'd want something for lvalues too. That unfortunately gets a little complicated:

template <class Tx, class Ux, 
    class = std::enable_if_t<std::is_convertible<std::decay_t<Tx>*, T*>::value &&
                             std::is_convertible<std::decay_t<Ux>*, U*>::value>>
Comp(Tx&& t, Ux&& u)
: t_m(std::forward<Tx>(t))
, u_m(std::forward<Ux>(u))
{ }

Here we want to allow deduction of Tx such that it's either T, T&, or D, D& where D derives from T. std::decay drops the reference and is_convertible for pointers checks if it's derived.

Okay, can we do better? Not really. This is either going to do 1 move or 1 copy for each member. But what if we want to construct them in place? That we should be able to allow for:

template <class... TArgs, class... UArgs,
    class = std::enable_if_t<std::is_constructible<T, TArgs...>::value &&
                             std::is_constructible<U, UArgs...>::value>>
Comp(std::piecewise_construct_t pc, std::tuple<TArgs...> const& t, std::tuple<UArgs...> const& u)
: Comp(t, u, std::index_sequence_for<TArgs...>{}, std::index_sequence_for<UArgs...>{})
{ }

private:
template <class TTuple, class UTuple, size_t... Is, size_t... Js>
Comp(TTuple const& t, UTuple const& u, std::index_sequence<Is...>, std::index_sequence<Js...> )
: t_m(std::get<Is>(t)...)
, u_m(std::get<Js>(u)...)
{ }

With this potentially we can avoid any kind of copy or move at all by just constructing in-place. Whether or not this is beneficial depends on your usage of Comp.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • Great! On a side note, is piecewise construction supported for `std::tuple` now ? (I haven't heard of this before and while searching I found [this](http://stackoverflow.com/q/11846634/4224575)) – Lorah Attkins Mar 29 '16 at 14:05
1

Your proposition with the move constructor seems to be the best approach.

As you deal with temporary objects, the best thing to do (and the most optimized) is to move the parameters in the members. But as you said maybe the move constructors may not exist.

If there is only one parameter it is easy to check if it is move constructible and move it and copy it otherwise. You can use std::enable_if with std::is_move_constructible.

But with more than 1 parameter, you have to check all the combinations. For instance for 2 parameters you have to have 4 constructors that do: copy/copy, move/copy, copy/move and move/move. So it is not really scalable and then it is more suitable to copy the parameters.

With the aggregate initialization the parameters are copy not move.

El pupi
  • 458
  • 1
  • 3
  • 13