"Inline" статическое объявление объекта с списком инициализаторов

Извините за неудобный титул, но я не смог найти лучшего.

Рассмотрим этот пример кода (у него нет цели, кроме иллюстрации вопроса):

#include <vector>

void FooBar(int);

void func1()
{
    static std::vector<int> vec {1, 2, 3, 4};

    for (auto & v : vec)
      FooBar(v);
}

void func2()
{
    for (auto & v : std::vector<int> {1, 2, 3, 4})
      FooBar(v);
}

Демонтаж этого можно найти здесь

В func1 статический vec вектор предполагается построить раз и навсегда при запуске. На самом деле демонтаж на godbolt, упомянутый выше, показывает, что инициализация статического vec выполняется только при первом вызове func1, а не при запуске, но это не точка здесь.

Теперь рассмотрим func2: здесь вектор прямо объявлен "inline" (не уверен, как это фактически вызывается) внутри оператора for, но, конечно, этот вектор создается каждый раз, когда вызывается func2.

Есть ли способ объявить этот вектор статически и внутри оператора for, например for (auto & v : static std::vector<int> { 1, 2, 3, 4}), который, к сожалению, не является законным С++.

Ответы

Ответ 1

Это пока не поможет. Но есть что-то, что вы можете сделать в С++ 2a. Это, однако, основано на том, что существует уже в С++ 14 и С++ 17.

С++ 2a добавил init-statement в диапазон, основанный на цикле. То, что новый бит, старый бит, это тот же init-statement, как он определен сегодня. И определение имеет следующий вид ([stmt.stmt]):

init-statement:
    expression-statement
    simple-declaration

Что я хочу сделать, так это то, что simple-declaration может содержать квалификатор static. Да, он делает то, что вы ожидаете. Итак, в С++ 2a вы можете написать:

for (static std::vector<int> vec {1, 2, 3, 4}; int v : vec) {
   // Do things.
}

И если вы хотите сегодня протестировать поддержку компилятора, здесь С++ 17 kludge:

if (static std::vector<int> vec {1, 2, 3, 4}; true) {
    // init-statement in if was added in C++17
  for(int v : vec)
    FooBar(v);
}

И его разборка.

Ответ 2

Другая опция, использующая лямбда (из комментариев по основному вопросу):

void func()
{
    for(auto & v : ([]() -> std::vector<int>& { static std::vector<int> vec{1, 2, 3, 4}; return vec; })())
        FooBar(v);
}

Ответ 3

Нет, в текущем С++ (С++ 17) это невозможно. Диапазон, основанный на значении, эквивалентен следующему псевдокоду:

{
    auto && __range = range_expression ;
    auto __begin = begin_expr ;
    auto __end = end_expr ;
    for ( ; __begin != __end; ++__begin) {
        range_declaration = *__begin;
        loop_statement
    }
} 

Здесь нет инструкции init. Выражение range_expression должно быть инициализировано либо априорно, либо вы должны пройти в список с привязкой к завершению, который должен быть оценен (например, std::vector<int>{1, 2, 3, 4}). Это будет изменено на С++ 20.

Ответ 4

Действительно, в стандарте С++ нет ничего, что бы запретить оптимизацию, которую вы ищете. Память кучи, которую выделяет std::vector<int,std::allocator>, действительно может быть заменена статической памятью без изменения поведения вашей программы. Но, как показывает ваша ссылка (даже если вы добавляете агрессивные параметры оптимизации), компиляторы не выполняют ожидаемую оптимизацию.

Таким образом, вы можете использовать std::array вместо std::vector, std::array легко "поняты" оптимизатором, здесь сборка:

void FooBar(int);

void func2()
{
  for (auto & v : std::array<int,4> {1, 2, 3, 4})
    FooBar(v);
}

Как вы видите в сборке, массив хранится в статической памяти:

.LC0:
    .long   1
    .long   2
    .long   3
    .long   4

Для удовольствия вы можете получить хорошую сборку, используя специальный распределитель, который использует статическую память, здесь сборка:

void FooBar(int i);

template<class T>
class static_alloc
  {
  static typename std::aligned_storage<4*sizeof(T),alignof(T)>::type buff;
  static bool allocated;

public:

  using value_type = T;

  bool operator==(const static_alloc&){
    return true;
  }
  bool operator!=(const static_alloc&){
    return false;
  }

  T* allocate(std::size_t n)
    {
    if (allocated) throw std::bad_alloc{};
    allocated=true;
    return reinterpret_cast<T*>(&buff);
    }
  void deallocate(T*,std::size_t)
    {
    allocated=false;
    }
  };
template<class T>
bool static_alloc<T>::allocated=false;
template<class T>
std::aligned_storage_t<4*sizeof(T),alignof(T)> static_alloc<T>::buff;


void func2()
{
   for (auto & v : std::vector<int,static_alloc<int>>{1,2,3,4})
      FooBar(v);
}

Ответ 5

Вы можете сделать лучше, чем это: вы можете просто иметь все в стеке. Если вы компилируете следующее на -O2 или выше, оно по существу разворачивается в 4 вызова на FooBar().

Кроме того, просмотр разборки с отключением оптимизации не имеет особого значения.

void func3()
{
    constexpr int vs [] = { 1, 2, 3, 4 };
    for ( int const v : vs )
        FooBar(v);
}