4

Often seen that examples of using STL algorithms are illustrated with list-initialized containers like:

std::vector< int > v{1, 2, 3, 4};

But when this approach is used for (heavyweight) classes (unlike ints) it implies excessive copy operations of ones, even if they are passed by rvalue (moved to), because std::initializer_list used in the example above provides only const_iterators.

To solve this problem I use the following (C++17) approach:

template< typename Container, typename ...Args >
Container make_container(Args &&... args)
{
    Container c;
    (c.push_back(std::forward< Args >(args)), ...);
    // ((c.insert(std::cend(c), std::forward< Args >(args)), void(0)), ...); // more generic approach
    return c;
}

auto u = make_container< std::vector< A > >(A{}, A{}, A{});

But it becames unsatisfactory when I do the following:

A a;
B b;
using P = std::pair< A, B >;
auto v = make_container< std::vector< P > >(P{a, b}, P{std::move(a), std::move(b)});

Here I want to save one copy operation per value by means of a replace a copy operation by a move operation (assume, to move A or B is much cheaper then to copy), but generally can't, because order of evaluation of function arguments is undefined in C++. My current solution is:

template< Container >
struct make_container
{

    template< typename ...Args >
    make_container(Args &&... args)
    {
        (c.push_back(std::forward< Args >(args)), ...);
    }

    operator Container () && { return std::move(c); }

private :

    Container c;

};

A a; B b;
using P = std::pair< A, B >;
using V = std::vector< P >;
V w = make_container< V >{P{a, b}, P{std::move(a), std::move(b)}};

It is often considered as a bad practice to make some non-trivial work in bodies of constructors, but here I intensively used the very property of list-initialization — the fact that it is strictly left-to-right ordered.

Is it totally wrong approach from some particular point of view? What are the downsides of this approach besides the one mentioned above? Is there another technique to achieve predictable order of evaluation of function arguments currently (in C++11, C++14, C++1z)?

8
  • While your approach works, I'd caution you against trying too hard to make (or emulate) function arguments with guaranteed order of evaluation. In terms of maintainability, future-you (and coworkers) will likely thank past-you if you avoid "magic" like this. Commented Mar 18, 2016 at 6:52
  • @Cornstalks Undoubtedly, It is predictable advice. But I looking for approach to achieve desired. Assume, it is good trade-off just here to use this non-standard approach ("magic") vs to follow principle "don't write a subtle code" Commented Mar 18, 2016 at 6:59
  • make_container< std::vector< A > >(a, std::move(b), std::move(a)); is fine. Remember that std::move is a cast, it does not move anything. Your function will receive A&, B&&, and A&&. If your function strictly moves out of arguments in that same order there is no problem. Commented Mar 18, 2016 at 7:01
  • @M.M It is interesting. You are right in case of operator , is sequenced (here for void result type of push_back for STL containers it does and can't be overloaded). What if is not? Say, the body of make function can use arguments in any order (or operator , is eventually overloaded for some use case). Commented Mar 18, 2016 at 7:05
  • If the body of make_container might push in reverse for example, then there is no possible solution (other than creating a temporary for every element), it seems to me Commented Mar 18, 2016 at 7:22

2 Answers 2

1

There's a better solution:

template<class Container, std::size_t N>
inline Container make_container(typename Container::value_type (&&a)[N])
{
    return Container(std::make_move_iterator(std::begin(a)), std::make_move_iterator(std::end(a)));
}

You can use it this way:

make_container<std::vector<A>>({A(1), A(2)})

It doesn't need variadic template and it's still list-initialization but not std::initializer_list, this time it's a plain array so you can move the elems from it.

Notable benefits compared to your original solution:

  • Better performance: it calls the Container's ctor directly which can give better performace (e.g. std::vector can reserve all the memory required)
  • Eval order guarantee: it's list-initialization

DEMO

Sign up to request clarification or add additional context in comments.

3 Comments

Super solution. Why STL uses std::initializer_list?
BTW class-based solution can be specialized for containers, which have reserve function. Your solution at second glance makes temporary copies, due to it is an equivalent of passing by value. In terms of copies and movies there is overhead of N excessive movies (for N parameters passed). Other downside is the fact, that you can't sequentially construct geterogeneous structure (AST node in my real life use case). Only container. But for container it is really good solution.
All the compilers I tested (msvc, g++, clang) are able to construct the elems directly in the temp array w/o calling the move-ctor at the first place, so I think practically the "temporary copies" are eliminable to the compiler. With regard to heterogeneous structures, they will have their variadic ctors which accept hetero args anyway, so you don't need such a helper function.
1

Is it totally wrong approach from some particular point of view?

It's unnecessarily hard to understand and can break badly if the called function ends up moving before copying. Remember, std::move does not move. It enables moves. Nothing more. It's the called function that ends up moving. If it processes parameters left to right, it'll work. If not, not.

Make it clear what's going on.

A a; A b;
using V = std::vector< A >;
A c {a};
V v = make_container< V >{std::move(a), b, std::move(c)};

6 Comments

Please, reread the question attentively once again. A c {a}; is excessive copying in context of the question. My approach intended to avoid this overhead.
@Orient You do not avoid copying. You copy from a once and move from it once. I make that copy explicit and do not introduce any other copies.
make_container::make_container arguments passed by reference, therefore you are not right.
Log the copy constructor calls if you like. The copy in my answer would otherwise have been done by your push_back.
My example gives A(A &), A(A &&), A(A &&) (if b also moved), in your case there will be A(A &), A(&&), A(&&), A(&&) (first A(A &) comes from A c {a};) - one excessive move construction.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.