Использование emplace с такими алгоритмами, как std:: fill

Я использовал vector::emplace_back, чтобы избежать создания временных объектов при заполнении вектора. Здесь у вас есть упрощенная версия:

class Foo {
public:
    Foo(int i, double d) : i_(i), d_(d) {}
    /* ... */
};

std::vector<Foo> v;
v.reserve(10);
for (int i = 0; i < 10; i++)
    v.emplace_back(1, 1.0);

Но вместо этого я хотел использовать std::fill_n:

v.reserve(10);
std::fill_n(std::back_inserter(v), 10, Foo(1, 1.0));

Таким образом, временные копии будут созданы. Я не знаю, как использовать emplace в этой ситуации. Думаю, мне понадобится что-то вроде std::back_emplacer, но я не мог найти такого. Является ли эта часть С++ 11, но еще не реализована в GCC? Если это не часть С++ 11, есть ли другой способ сделать это?

Ответ 1

Общепринято использовать кортежи, чтобы облегчить прохождение вариационного количества элементов (в этом случае параметры для перехода на emplace_back), немного техники распакуйте кортеж назад. Таким образом, можно написать утилиту back_emplacer, требуя от пользователя использовать функции tuple factory (один из std::make_tuple, std::tie, std::forward_as_tuple), где это имеет смысл:

#include <type_traits>
#include <tuple>

// Reusable utilites

template<typename T>
using RemoveReference = typename std::remove_reference<T>::type;
template<typename T>
using Bare = typename std::remove_cv<RemoveReference<T>>::type;

template<typename Out, typename In>
using WithValueCategoryOf = typename std::conditional<
    std::is_lvalue_reference<In>::value
    ,  typename std::add_lvalue_reference<Out>::type
    , typename std::conditional<
        std::is_rvalue_reference<Out>::value
        , typename std::add_rvalue_reference<Out>::type
        , Out
    >::type
>::type;

template<int N, typename Tuple>
using TupleElement = WithValueCategoryOf<
    typename std::tuple_element<N, RemoveReference<Tuple>>::type
    , Tuple
>;  

// Utilities to unpack a tuple
template<int... N>
struct indices {
    using next = indices<N..., sizeof...(N)>;
};

template<int N>
struct build_indices {
    using type = typename build_indices<N - 1>::type::next;
};
template<>
struct build_indices<0> {
    using type = indices<>;
};

template<typename Tuple>
constexpr
typename build_indices<std::tuple_size<Bare<Tuple>>::value>::type
make_indices() { return {}; }

template<typename Container>
class back_emplace_iterator {
public:
    explicit back_emplace_iterator(Container& container)
        : container(&container)
    {}  

    template<
        typename Tuple
        // It important that a member like operator= be constrained
        // in this case the constraint is delegated to emplace,
        // where it can more easily be expressed (by expanding the tuple)   
        , typename = decltype( emplace(std::declval<Tuple>(), make_indices<Tuple>()) )
    >
    back_emplace_iterator& operator=(Tuple&& tuple)
    {
        emplace(*container, std::forward<Tuple>(tuple), make_indices<Tuple>());

        return *this;
    }

    template<
        typename Tuple
        , int... Indices  
        , typename std::enable_if<
            std::is_constructible<
                typename Container::value_type
                , TupleElement<Indices, Tuple>...
            >::value
            , int
        >::type...
    >
    void emplace(Tuple&& tuple, indices<Indices...>)
    {
        using std::get;
        container->emplace_back(get<Indices>(std::forward<Tuple>(tuple))...);
    }

    // Mimic interface of std::back_insert_iterator
    back_emplace_iterator& operator*() { return *this; }
    back_emplace_iterator& operator++() { return *this; }
    back_emplace_iterator operator++(int) { return *this; }

private:
    Container* container;  
};

template<typename Container>
back_emplace_iterator<Container> back_emplacer(Container& c)
{ return back_emplace_iterator<Container> { c }; }

Демонстрация кода доступна. В вашем случае вы хотите вызвать std::fill_n(back_emplacer(v), 10, std::forward_as_tuple(1, 1.0)); (std::make_tuple также допустимо). Вы также хотите, чтобы обычный материал итератора сделал эту функцию полной - я рекомендую Boost.Iterators для этого.

Я должен действительно подчеркнуть, что такая утилита не приносит большого значения при использовании с std::fill_n. В вашем случае это сохранит конструкцию временного Foo, в пользу кортежа ссылок (кортеж значений, если вы должны использовать std::make_tuple). Я оставляю его читателю, чтобы найти другой алгоритм, в котором back_emplacer был бы полезен.

Ответ 2

Вы правы, что в стандарте нет back_emplacer. Вы могли бы написать его самостоятельно, но зачем?

Когда вы вызываете emplace_back, вы должны предоставить аргументы для конструктора (любого конструктора): vec.emplace_back(1, 2) например. Однако вы не можете произвольно передавать кортежи аргументов в С++, поэтому back_emplacer будет ограничен унарным конструктором.

В случае fill_n вы указываете аргумент, который будет скопирован, и оба back_inserter и back_emplacer будут вызывать тот же конструктор копии с тем же аргументом.

Обратите внимание, что для создания новых элементов существуют алгоритмы generate и generate_n. Но, вероятно, любая временная копия, вероятно, будет устранена.

Поэтому я считаю, что потребность в back_emplacer довольно легкая, главным образом из-за отсутствия поддержки несколькими возвращаемыми значениями языка.

ИЗМЕНИТЬ

Если вы посмотрите на приведенные ниже комментарии, вы поймете, что с помощью комбинации std::forward_as_tuple и std::is_constructible можно написать механизм back_emplacer. Спасибо Люку Дантону за прорыв.

Ответ 3

class Foo {
public:
  Foo(int i, double d) : i_(i), d_(d) {}
};

std::vector<Foo> v;
v.reserve(10);
std::generate_n(std::back_inserter(v), 10, [&]()->Foo{ return {1, 1.0}; });

RVO позволяет вернуть возвращаемое значение функции непосредственно туда, где она будет сохранена.

Хотя логически создается временное, на самом деле временное создание не создается. И у вас есть доступ ко всем переменным в окружающей области, чтобы решить, как создать элемент, а не только константы, если вы хотите их.

Ответ 4

Никаких "временных копий" не будет. Будет только один временный, тот, который вы передали fill_n. И он будет скопирован в каждое значение.

И даже если бы был back_emplacer, как бы вы его назвали? Семейство функций emplace принимает параметры конструктора; fill_n принимает объект для копирования в итератор.

Ответ 5

Недавно я представил класс emplace_iterator и связанную с ним функцию полезности в библиотеку безумия. Я считаю, что он решает исходный вопрос и поддерживает автоматическое расстегивание аргументов std::tuple, переданных в operator=.

https://github.com/facebook/folly/blob/master/folly/Iterator.h

class Widget { Widget(int, int); };

std::vector<Widget> makeWidgets(const std::vector<int>& in) {
  std::vector<Widget> out;
  std::transform(
      in.begin(),
      in.end(),
      folly::back_emplacer(out),
      [](int i) { return folly::make_emplace_args(i, i); });
  return out;
}

folly::make_emplace_args аналогичен std::make_tuple, но приводит к идеальной пересылке его аргументов конструктору Widget. (std::make_tuple и аналогичные могут привести к дополнительным копиям и не сохранять lvalue vs rvalue typedness.) В этом конкретном примере использование std::make_tuple будет иметь такой же эффект.