vukung
vukung

Reputation: 1884

Efficient direct initialization of a std::vector

I have a struct, say

struct A {
  A(int n) : n(n) {}
  int n;
};

and I want to initialize a std::vector with some elements. I can do this by using an initialization list, or by emplacing the new elements:

// 1st version: 3 ctors, 3 copy ctors, 3 dtors                                           
std::vector<A> v1{1, 2, 3};

// 2nd version: 3 ctors                                                                  
std::vector<A> v2;
v2.reserve(3);
v2.emplace_back(4);
v2.emplace_back(5);
v2.emplace_back(6);

As the comments show, the first version calls 3 constructors, 3 copy constructors, and 3 destructors. The version with emplace only uses 3 constructors.

Question: Obviously the second version is better, but the first version is more succinct. Can I have the best of both worlds? Can I do a direct initialization without the extra cost?

(Here's a longer version of the A struct that shows what's happening.)

Upvotes: 3

Views: 1444

Answers (2)

Aaron McDaid
Aaron McDaid

Reputation: 27193

Expanding on @ecatmur's answer, I've developed a piece of code that allows a very general solution for any type of vector and for any constructor calls. The constructor parameters for each element of the vector are stored in a tuple (of & or && as appropriate) which are then perfect-forwarded when the element is built. Each element is constructed only once, essentially equivalent to emplace_back. This forwarding would even allow a vector of move-only types to be built such as unique_ptr<?>.

(Update, due to RVO it should simply construct them in place. Unfortunately, however, the element type does require at least a copy-constructor or move-constructor to be visible, even if they are actually skipped by the optimizer. This means you can build a vector of unique_ptr, but not of mutex.)

auto v2 = make_vector_efficiently<A>(
         pack_for_later(1)        // 1-arg constructor of A
        ,pack_for_later(2,"two")  // 2-arg constructor of A
        ,pack_for_later(3)        // 1-arg constructor of A
        );

The above code will create a vector<A> with three elements. In my example, A has two constructors, one which takes int,string as parameters.

pack_for_later builds a tuple that stores its parameters as &/&& references. That is then converted into on object (of type UniquePointerThatConverts, that has the desired conversion operator, in this case operator A().

Within make_vector_efficiently, an initializer list of these converter objects is built and then vector is constructed with the begin() and end() of the initializer_list. You might expect that these iterators would be required to have type T* in order to construct a vector<T>, but it is sufficient that the type the iterator points to can convert to T.

The constructor then uses placement new to (copy-)construct from the converted object. But, thanks for RVO, the copy won't happen and the converter will be effectively doing the equivalent of emplace_back for us.

Anyway, any feedback appreciated. Finally, it's trivial to extend this to other containers besides vector.


Full code on Coliru Should work on any C++11 compiler.


Some more detailed notes, and copies of the important functions:

pack_for_later simply builds a std::tuple. The standard make_tuple isn't good enough as it ignores references. Each element of the tuple built by pack_for_later is a reference (& or && as appropriate, depending on whether the original parameter was an lvalue or rvalue)

template<typename ...T> 
std:: tuple<T&&...> pack_for_later(T&&... args) { 
        // this function is really just a more
        // 'honest' make_tuple - i.e. without any decay
    return std:: tuple<T&&...> (std::forward<T>(args)...);
} 

Next, make_vector_efficiently is the function that brings is all together. It's first parameter is the 'Target' type, the type of the elements in the vector we wish to create. The collection of tuples is converted into our special converter type UniquePointerThatConverts<Target> and the vector is constructed as discussed above.

template<typename Target, typename ...PackOfTuples>
auto make_vector_efficiently(PackOfTuples&&... args)
    -> std::vector<Target>
{
    auto inits = { UniquePointerThatConverts<Target>(std::forward<PackOfTuples>(args))...};
    return std::vector<Target> {std::begin(inits), std::end(inits)};
}

Because A can have multiple constructors, and we want to be able to use any and all of them, pack_for_later can return many different types (don't forget about lvalues and rvalues too). But we need a single type to build the init list from. Therefore, we define a suitable interface:

template<typename Target>
struct ConvInterface {
    virtual Target convert_to_target_type() const = 0;
    virtual ~ConvInterface() {}
};

Each tuple is therefore converted to an object that implements this interface by make_Conv_from_tuple. It actually returns a unique_ptr to such an object which is then stored in a UniquePointerThatConverts that has the actual conversion operator. It is this type that is stored in the init list which is used to initialize the vector.

template<typename Target>
struct UniquePointerThatConverts {
    std:: unique_ptr<ConvInterface<Target>> p; // A pointer to an object
               // that implements the desired interface, i.e.
               // something that can convert to the desired
               // type (Target).

    template<typename Tuple>
    UniquePointerThatConverts(Tuple&& p_)
    : p ( make_Conv_from_tuple<Target>(std:: move(p_)) )
    {
        //cout << __PRETTY_FUNCTION__ << endl;
    }
    operator Target () const {
        return p->convert_to_target_type();
    }
};

And, of course, the actual conversion operator which constructs from the pack.

template<typename Target, typename ...T>
struct Conv : public ConvInterface<Target> {
    std::tuple<T...> the_pack_of_constructor_args;
    Conv(std::tuple<T...> &&t) : the_pack_of_constructor_args(std:: move(t)) {}

    Target convert_to_target_type () const override {
        using idx = typename make_my_index_sequence<sizeof...(T)> :: type;
        return foo(idx{});
    }
    template<size_t ...i>
    Target foo(my_index_sequence<i...>) const {
        // This next line is the main line, constructs
        // something of the Target type (see the return
        // type here) by expanding the tuple.
        return {
                std:: forward
                    < typename std:: tuple_element < i , std::tuple<T...> > :: type >
                    (std:: get<i>(the_pack_of_constructor_args))
            ...
        };
    }
};

Upvotes: 2

ecatmur
ecatmur

Reputation: 157524

Since A is convertible from int, you can use the range constructor of vector:

auto inits = {1, 2, 3};
std::vector<A> v1{std::begin(inits), std::end(inits)};

Or in a single declaration-statement (assuming you can rely on RVO):

auto v1 = [inits={1, 2, 3}] { return std::vector<A>{std::begin(inits), std::end(inits)}; }();

Upvotes: 5

Related Questions