Nano Hash - криптовалюты, майнинг, программирование

Передача аргументов конструктору контейнера, похожему на массив

Фон

Я работаю со встроенной платформой со следующими ограничениями:

  • Нет кучи
  • Нет библиотек Boost
  • C++11 поддерживается

В прошлом я несколько раз сталкивался со следующей проблемой:

Создайте массив типа класса T, где T не имеет конструктора по умолчанию

В проект только недавно была добавлена ​​поддержка C++11, и до сих пор я использовал специальные решения каждый раз, когда мне приходилось с этим сталкиваться. Теперь, когда доступен С++ 11, я решил попробовать сделать более общее решение.

Попытка решения

Я скопировал пример std::aligned_storage, чтобы придумать основу для моего тип массива. Результат выглядит следующим образом:

#include <type_traits>

template<class T, size_t N>
class Array {
  // Provide aligned storage for N objects of type T
  typename std::aligned_storage<sizeof(T), alignof(T)>::type data[N];

public:
  // Build N objects of type T in the aligned storage using default CTORs
  Array()
  {
    for(auto index = 0; index < N; ++index)
      new(data + index) T();
  }

  const T& operator[](size_t pos) const
  {
    return *reinterpret_cast<const T*>(data + pos);
  }

  // Other methods consistent with std::array API go here
};

Это базовый тип — Array<T,N> компилируется только в том случае, если T конструируется по умолчанию. Я не очень хорошо знаком с упаковкой параметров шаблона, но просмотр некоторых примеров привел меня к следующему:

template<typename ...Args>
Array(Args&&... args) 
{
  for(auto index = 0; index < N; ++index)
    new(data + index) T(args...);
}

Это определенно был шаг в правильном направлении. Array<T,N> теперь компилируется, если переданные аргументы соответствуют конструктору T.

Моя единственная оставшаяся проблема - это создание Array<T,N>, где разные элементы в массиве имеют разные аргументы конструктора. Я подумал, что могу разделить это на два случая:

1 - Пользователь указывает аргументы

Вот мой удар по CTOR:

template<typename U>
Array(std::initializer_list<U> initializers)
{
  // Need to handle mismatch in size between arg and array
  size_t index = 0;
  for(auto arg : initializers) {
    new(data + index) T(arg);
    index++;
  }
}

Кажется, это работает нормально, за исключением необходимости обрабатывать несоответствие размеров между массивом и списком инициализаторов, но есть несколько способов справиться с этим, которые не важны. Вот пример:

struct Foo {
  explicit Foo(int i) {}
};

void bar() {
  // foos[0] == Foo(0)
  // foos[1] == Foo(1)
  // ..etc
  Array<Foo,10> foos {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
}

2. Аргументы следуют шаблону

В моем предыдущем примере foos инициализируется возрастающим списком, подобным std::iota. В идеале я хотел бы поддерживать что-то вроде следующего, где range(int) возвращает ЧТО-ТО, что может инициализировать массив.

// One of these should initialize foos with parameters returned by range(10)
Array<Foo,10> foosA = range(10);
Array<Foo,10> foosB {range(10)};
Array<Foo,10> foosC = {range(10)};
Array<Foo,10> foosD(range(10));

Поиск в Google показал мне, что std::initializer_list не является «нормальным» контейнером, поэтому я не думаю, что у меня есть какой-либо способ заставить range(int) возвращать std::initializer_list в зависимости от параметра функции.

Опять же, здесь есть несколько вариантов:

  • Параметры, указанные во время выполнения (возврат функции?)
  • Параметры, указанные во время компиляции (constexpr возврат функции? шаблоны?)

Вопросы

  1. Есть ли какие-либо проблемы с этим решением до сих пор?
  2. Есть ли у кого-нибудь предложение создать параметры конструктора? Я не могу придумать решение во время выполнения или во время компиляции, кроме жесткого кодирования std::initializer_list, поэтому приветствуются любые идеи.

  • почему бы не использовать std:array? Он идеально соответствует вашим требованиям 26.08.2016
  • Вы можете использовать vector с настраиваемым распределителем, который сопоставляет хранилище с любой областью хранилища, которую вы выделили для этой цели. 26.08.2016
  • @GuillaumeRacicot Может ли std::array иметь дело со вторым типом инициализации, о котором я упоминал? В моем примере я мог бы сказать: std::array<Foo,3> = { Foo(0), Foo(1), Foo(2) }; Хотя я не знаю, как это можно расширить. Если я сделаю функцию, возвращающую { Foo(0), Foo(1), Foo(2) } как std::initializer_list<Foo>, и попытаюсь использовать ее для инициализации std::array, я получу ошибку. Могу ли я использовать std::array без жесткого кодирования всех элементов? Что, если Foo нельзя скопировать? 26.08.2016
  • @M.M Да, я подумал, что что-то в этом роде сработает. Я не большой поклонник этого, поскольку контейнер должен вести себя как массив, а не как вектор. Я бы предпочел не раскрывать std::vector API, если контейнер предназначен для фиксированного размера с постоянными (относительно времени существования контейнера) элементами. 26.08.2016
  • @Matt std::array не используйте std::initializer_list, а агрегируйте инициализацию, а это разные вещи. Например, у вас может быть std::array<std::unique_ptr<int>, 2> myArray{std::make_unique<int>(), std::make_unique<int>()}, и он будет правильно построен. 26.08.2016
  • @GuillaumeRacicot Я понимаю разницу между ними - я думаю, что единственные проблемы, с которыми я сталкиваюсь, - это потому что std::array использует агрегатную инициализацию. В вашем примере myArray работает, потому что тип возвращаемого значения std::make_unique<int> соответствует типу элемента std::array<std::unique_ptr<int>,2>. Это накладывает несколько ограничений на std::array<T,N>: 1. T должен быть перемещаемым или копируемым 2. Список инициализаторов не может быть возвращен функцией. Я не думаю, что есть обходные пути для этих проблем. Если бы они были, я бы согласился, что std::array подходит для этой работы. 26.08.2016

Ответы:


1

Если я правильно понимаю вашу проблему, я также наткнулся на полную негибкость std::array в отношении построения элементов в пользу агрегатной инициализации (и отсутствие статически распределенного контейнера с гибкими параметрами построения элементов). Лучший подход, который я придумал, заключался в создании пользовательского контейнера, похожего на массив, который принимает итератор для создания своих элементов. Это полностью гибкое решение:

  • Работает как с контейнерами фиксированного размера, так и с контейнерами динамического размера.
  • Может передавать разные или одинаковые параметры конструкторам элементов
  • Может вызывать конструкторы с одним или несколькими (кортежное кусочное построение) аргументами или даже разными конструкторами для разных элементов (с инверсией управления)

Для вашего примера это будет так:

const size_t SIZE = 10;

std::array<int, SIZE> params;
for (size_t c = 0; c < SIZE; c++) {
    params[c] = c;
}

Array<Foo, SIZE> foos(iterator_construct, &params[0]); //iterator_construct is a special tag to call specific constructor
// also, we are able to pass a pointer as iterator, since it has both increment and dereference operators

Примечание: вы можете полностью пропустить выделение массива параметров здесь, используя пользовательский класс итератора, который вычисляет его значение из его позиции на лету.

Для конструктора с несколькими аргументами это будет:

const size_t SIZE = 10;

std::array<std::tuple<int, float>, SIZE> params; // will call Foo(int, float)
for (size_t c = 0; c < SIZE; c++) {
    params[c] = std::make_tuple(c, 1.0f);
}

Array<Foo, SIZE> foos(iterator_construct, piecewise_construct, &params[0]);

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

26.08.2016

2

Я бы использовал заводскую лямбду.

Лямбда принимает указатель на место построения и индекс и отвечает за построение.

Это также упрощает копирование/перемещение, что является хорошим знаком.

template<class T, std::size_t N>
struct my_array {
  T* data() { return (T*)&buffer; }
  T const* data() const { return (T const*)&buffer; }

  // basic random-access container operations:
  T* begin() { return data(); }
  T const* begin() const { return data(); }
  T* end() { return data()+N; }
  T const* end() const { return data()+N; }
  T& operator[](std::size_t i){ return *(begin()+i); }
  T const& operator[](std::size_t i)const{ return *(begin()+i); }

  // useful utility:
  bool empty() const { return N!=0; }
  T& front() { return *begin(); }
  T const& front() const { return *begin(); }
  T& back() { return *(end()-1); }
  T const& back() const { return *(end()-1); }
  std::size_t size() const { return N; }

  // construct from function object:
  template<class Factory,
    typename std::enable_if<!std::is_same<std::decay_t<Factory>, my_array>::value, int> =0
  >
  my_array( Factory&& factory ) {
    std::size_t i = 0;
    try {
      for(; i < N; ++i) {
        factory( (void*)(data()+i), i );
      }
    } catch(...) {
      // throw during construction.  Unroll creation, and rethrow:
      for(std::size_t j = 0; j < i; ++j) {
        (data()+i-j-1)->~T();
      }
      throw;
    }
  }
  // other constructors, in terms of above naturally:
  my_array():
    my_array( [](void* ptr, std::size_t) {
      new(ptr) T();
    } )
  {}
  my_array(my_array&& o):
    my_array( [&](void* ptr, std::size_t i) {
      new(ptr) T( std::move(o[i]) );
    } )
  {}
  my_array(my_array const& o):
    my_array( [&](void* ptr, std::size_t i) {
      new(ptr) T( o[i] );
    } )
  {}
  my_array& operator=(my_array&& o) {
    for (std::size_t i = 0; i < N; ++i)
      (*this)[i] = std::move(o[i]);
    return *this;
  }
  my_array& operator=(my_array const& o) {
    for (std::size_t i = 0; i < N; ++i)
      (*this)[i] = o[i];
    return *this;
  }
private:
  using storage = typename std::aligned_storage< sizeof(T)*N, alignof(T) >::type;
  storage buffer;
};

он определяет my_array(), но компилируется, только если вы пытаетесь его скомпилировать.

Поддержка списка инициализаторов относительно проста. Трудно решить, что делать, когда il недостаточно длинный или слишком длинный. Я думаю, вы можете захотеть:

template<class Fail>
my_array( std::initializer_list<T> il, Fail&& fail ):
  my_array( [&](void* ptr, std::size_t i) {
    if (i < il.size()) new(ptr) T(il[i]);
    else fail(ptr, i);
  } )
{}

который требует, чтобы вы передали «что делать при сбое». Мы могли бы по умолчанию использовать throw, добавив:

template<class WhatToThrow>
struct throw_when_called {
  template<class...Args>
  void operator()(Args&&...)const {
    throw WhatToThrow{"when called"};
  }
};

struct list_too_short:std::length_error {
  list_too_short():std::length_error("list too short") {}
};
template<class Fail=ThrowWhenCalled<list_too_short>>
my_array( std::initializer_list<T> il, Fail&& fail={} ):
  my_array( [&](void* ptr, std::size_t i) {
    if (i < il.size()) new(ptr) T(il[i]);
    else fail(ptr, i);
  } )
{}

что, если я написал это правильно, приводит к тому, что слишком короткий список инициализаторов вызывает осмысленное сообщение о выбросе. На вашей платформе вы можете просто exit(-1), если у вас нет исключений.

26.08.2016
Новые материалы

Кластеризация: более глубокий взгляд
Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

Как написать эффективное резюме
Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

Частный метод Python: улучшение инкапсуляции и безопасности
Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

Как я автоматизирую тестирование с помощью Jest
Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..