С++/С++ 11 Эффективный способ создания статического массива/вектора объектов, инициализированных списком инициализаторов, и поддержки диапазона для

Предположим, вы хотите иметь статический массив предварительно определенных значений/объектов (const или non-const), связанных с классом. Возможные варианты использования std:vector, std::array или C-style array (ie. []), или. Например,

В .hpp:

class MyClass {
public:
    static const std::vector<MyClass> vec_pre; // No efficient way to construct with initializer list, since it always uses Copy Contructor, even when using std::move
    static const std::array<MyClass, 2> arr_pre; // Have to specify size which is inconvenient
    static const MyClass carr_pre[]; // Not compatible with C++11 for-range since size is undefined
};

В .cpp

const std::vector<MyClass> MyClass::vec_pre = { std::move(MyClass{1,2,3}), std::move(MyClass{4,5,6})  }; // NOTE: This still uses copy constructor
const std::array<MyClass, 2> MyClass::arr_pre= { MyClass{1,2,3}, MyClass{4,5,6} };
const ZwSColour ZwSColour::carr_pre[] = {  MyClass{1,2,3}, MyClass{1,2,3} }

При написании этого я выбрал std::vector, так как мне не нужно указывать размер, я получаю всю доброту векторного класса, и это похоже на современный С++ способ сделать это. ПРОБЛЕМА: во время тестирования я заметил, что он вызовет конструктор Move, но затем все равно вызовет конструктор Copy для каждого элемента. Причиной этого является std::initializer_list только разрешает доступ к const своим членам, поэтому вектор должен скопировать их из списка initializer_list в собственное хранилище. Несмотря на то, что это было сделано только один раз при запуске, , это неэффективно и, похоже, не существует способа, поэтому я просмотрел другие параметры (std::array и C-array[]).

Второй выбор заключался в использовании std::array, который также является современным С++-способом, и он не страдает проблемой вызова Copy Constructor для каждого значения, поскольку ему не нужно создавать копии (не знаете, почему хотя точно?). std::array также имеет то преимущество, что вам не нужно обертывать каждое значение в std::move(). Тем не менее, у вас есть досада, что вы должны сначала указать размер, поэтому каждый раз, когда вы добавляете/удаляете элементы, вы также должны изменять размер. Есть способы обойти это, но ни один из них не идеален. Как утверждает @Ricky65, вы должны просто иметь возможность делать

std::array <int> arr = { 1, 3, 3, 7, 0, 4, 2, 0, 3, 1, 4, 1, 5, 9 }; //automatically deduces its size from the initializer list :)

Это оставляет мне последний вариант - старый старый массив C-style [], который имеет те преимущества, которые мне не нужно указывать, и эффективен тем, что он не вызывает конструктор Copy для каждый объект. Недостатки в том, что это не очень современный С++, и самым большим недостатком является то, что если вы не укажете размер массива в заголовке .hpp, тогда С++ 11 для диапазона не будет работать, поскольку компилятор жалуется

Нельзя использовать неполный тип 'const MyClass []' как диапазон

Вы можете преодолеть эту ошибку, указав размер массива в заголовке (но это неудобно и создает сложный код, так как вам нужно настроить размер каждый раз, когда вы добавляете/удаляете элементы из инициализатора список), или альтернативно использовать constexpr и полностью объявить массив и значения в заголовке .hpp

constexpr static MyArray my_array[] = { MyClass{1,2,3}, MyClass{4,5,6} };

ПРИМЕЧАНИЕ. Консольс "work-around" работает только для POD и поэтому не может использоваться в этом случае для объектов класса. Приведенный выше пример приведет к ошибке времени компиляции Invalid use of incomplete type 'MyClass'

Я пытаюсь писать лучшие на практике С++, где это возможно (например, используя идиому копирования и свопинга), и поэтому задайтесь вопросом, что является лучшим способом для определения статических массивов для класса...

  • без указания размера
  • который не обязательно должен быть скопирован (или Move сконструирован либо, если это возможно)
  • который может использоваться с С++ для диапазона
  • которые не обязательно должны указываться в файле заголовка
  • Должен компилироваться/работать для Clang/LLVM 3.5, Visual Studio 2013 Update 4 RC и GCC 4.8.1.

EDIT1: Еще одно сообщение об векторной проблеме невозможности перемещения значений из списка инициализаторов

EDIT2: Дополнительная информация об использовании std:: массива без указания размера, который также создает/использует make_array() и упоминает, что существует предложение для make_array(), чтобы стать стандартом. Оригинальная ссылка SO, предоставленная комментарием @Neil Kirk.

EDIT3: Еще одна проблема с методом vector (по крайней мере в этом случае) заключается в том, что вы не можете перебирать элементы с помощью const T или T. Он допускает итерацию только с помощью const T& (когда он static const) и const T&/T& (когда он static). Какова причина этого ограничения?

Описательный ответ на решения

@Yakk решение является единственным решением, а также работает на Visual С++ 2013 Update 4 RC.

Я нахожу это ошеломляющим, что такую ​​тривиальную проблему сложно реализовать с использованием последнего стандарта С++ 11/14.

Ответы

Ответ 1

Данные не должны храниться в классе. Фактически, хранение данных в члене static класса приводит к утечке деталей реализации.

Все, что вам нужно, - это доступность данных и глобальные данные для типа класса. Это не связано с раскрытием сведений о хранении: все, что вам нужно предоставить, - это сведения о доступе к хранилищу.

В частности, вы хотите выставить возможность цикла for(:) по данным и работать с ним в стиле С++ 11. Так что выставляйте именно это.

Храните данные в анонимном пространстве имен в файле класса .cpp в массиве C-стиля (или std::array, мне все равно).

Выставить в классе следующее:

namespace details {
  template<
    class R,
    class iterator_traits,
    class iterator_category,
    bool is_random_access=std::is_base_of<
        std::random_access_iterator_tag,
        iterator_category
    >::value
  >
  struct random_access_support {};
  template<class R, class iterator_traits, class iterator_category>
  struct random_access_support<R, iterator_traits, iterator_category, true> {
    R const* self() const { return static_cast<R const*>(this); }
    template<class S>
    typename iterator_traits::reference operator[](S&&s) const {
      return self()->begin()[std::forward<S>(s)];
    }
    std::size_t size() const { return self()->end()-self()->begin(); }
  };
}

template<class It>
struct range:details::random_access_support<
  range<It>,
  std::iterator_traits<It>,
  typename std::iterator_traits<It>::iterator_category
> {
  using value_type = typename std::iterator_traits<It>::value_type;
  using reference = typename std::iterator_traits<It>::reference;
  using iterator = It;
  using iterator_category = typename std::iterator_traits<It>::iterator_category;
  using pointer = typename std::iterator_traits<It>::pointer;

  It begin() const { return b; }
  It end() const { return e; }

  bool empty() const { return b==e; }
  reference front() const { return *b; }
  reference back() const { return *std::prev(e); }

  range( It s, It f ):b(s),e(f) {}

  range()=default;
  range(range const&)=default;
  range& operator=(range const&)=default;
private:
  It b; It e;
};

namespace details {
  template<class T>
  struct array_view_helper:range<T*> {
    using non_const_T = typename std::remove_const<T>::type;
    T* data() const { return this->begin(); }

    array_view_helper( array_view_helper const& ) = default;
    array_view_helper():range<T*>(nullptr, nullptr){}
    array_view_helper& operator=(array_view_helper const&)=default;

    template<class A>
    explicit operator std::vector<non_const_T, A>() const {
      return { this->begin(), this->end() };
    }
    std::vector<non_const_T> as_vector() const {
      return std::vector<non_const_T>(*this);
    }

    template<std::size_t N>
    array_view_helper( T(&arr)[N] ):range<T*>(arr+0, arr+N) {}
    template<std::size_t N>
    array_view_helper( std::array<T,N>&arr ):range<T*>(arr.data(), arr.data()+N) {}
    template<class A>
    array_view_helper( std::vector<T,A>&vec ):range<T*>(vec.data(), vec.data()+vec.size()) {}
    array_view_helper( T*s, T*f ):range<T*>(s,f) {}
  };
}
// non-const
template<class T>
struct array_view:details::array_view_helper<T> {
  using base = details::array_view_helper<T>;

  // using base::base in C++11 compliant compilers:
  template<std::size_t N>
  array_view( T(&arr)[N] ):base(arr) {}
  template<std::size_t N>
  array_view( std::array<T,N>&arr ):base(arr) {}
  template<class A>
  array_view( std::vector<T,A>&vec ):base(vec) {}
  array_view( T*s, T*f ):base(s,f) {}

  // special methods:
  array_view( array_view const& ) = default;
  array_view() = default;
  array_view& operator=(array_view const&)=default;
};
template<class T>
struct array_view<T const>:details::array_view_helper<const T> {
  using base = details::array_view_helper<const T>;

  // using base::base in C++11 compliant compilers:
  template<std::size_t N>
  array_view( std::array<T const,N>&arr ):base(arr) {}
  array_view( T const*s, T const*f ):base(s,f) {}
  template<std::size_t N>
  array_view( T const(&arr)[N] ):base(arr) {}

  // special methods:
  array_view( array_view const& ) = default;
  array_view() = default;
  array_view& operator=(array_view const&)=default;

  // const T only constructors:
  template<std::size_t N>
  array_view( std::array<T,N> const&arr ):base(arr.data(), arr.data()+N) {}
  template<std::size_t N>
  array_view( std::array<T const,N> const&arr ):base(arr.data(), arr.data()+N) {}
  template<class A>
  array_view( std::vector<T,A> const&vec ):base(vec.data(), vec.data()+vec.size()) {}
  array_view( std::initializer_list<T> il):base(il.begin(), il.end()) {}
};

который является, по крайней мере, эскизом некоторых классов представления. живой пример

Затем выведите array_view<MyClass> в качестве члена static вашего класса, который инициализируется массивом, созданным в файле .cpp.

range<It> - это диапазон итераторов, который действует как контейнер, не являющийся владельцем. Выполняется некоторая глупость, чтобы блокировать вызовы, не связанные с постоянным временем, на size или [] на уровне SFINAE. back() отображается и просто не скомпилируется, если вы вызываете его на недействительных итераторах.

A make_range(Container) делает range<It> более полезным.

array_view<T> является range<T*>, который имеет кучу конструкторов из смежных буферных контейнеров, таких как C-массивы, std::array и std::vector s. (фактически исчерпывающий список).

Это полезно, потому что доступ через array_view примерно так же эффективен, как доступ к исходному указателю-первому элементу массива, но мы получаем много хороших методов, которые есть в контейнерах, и он работает с диапазоном для петель. В общем случае, если функция принимает std::vector<T> const& v, вы можете заменить ее на функцию, которая принимает array_view<T> v, и она будет заменой. Большим исключением является operator vector, который является явным, чтобы избежать случайных распределений.

Ответ 2

Мне лично нравится ваш constexpr static int my_array[] = {MyClass{1, 2, 3}, MyClass{1, 2, 3}};. Я не думаю, что вам следует уклониться от этого, если массив C-стиля соответствует вашим потребностям.

Если вы действительно хотите использовать std::vector, хотя вы можете использовать static const std::vector<MyClass*> vec_pre;. Таким образом, ваш файл .cpp будет иметь это вверху:

namespace{
    MyClass A{1, 2, 3}, B{1, 2, 3}, C{1, 2, 3};
}
const std::vector<MyClass*> MyClass::vec_pre{&A, &B, &C};

ИЗМЕНИТЬ после DarkMatter комментариев:

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

namespace{
    MyClass temp[]{MyClass{1, 2, 3}, MyClass{1, 2, 3}, MyClass{1, 2, 3}};
    const MyClass* pTemp[]{&temp[0], &temp[1], &temp[2]};
}
const std::vector<MyClass*> MyClass::vec_pre{begin(pTemp), end{pTemp}};

Вы также можете удалить дублирование проблемы ремонтопригодности записи, создав макрос для этого.

Ответ 3

Вот способ установки вектора без копирования или перемещения.

Он не использует строчный инициализатор, но ваш вступительный абзац предполагает, что ваша главная задача - избегать копий и движений; а не абсолютное требование использовать фиксированный инициализатор.

 // header
const std::vector<MyClass> &get_vec();

// cpp file
const std::vector<MyClass> &get_vec()
{
    static std::vector<MyClass> x;

    if ( x.empty() )
    {
        x.emplace_back(1,2,3);
        x.emplace_back(4,5,6);
        // etc.
    }

    return x;    
}