Създаване на неинициализиран масив от елементи, които имат конструктори по подразбиране?

Даден е клас Foo, който има някакъв конструктор по подразбиране, инициализиращ стойността:

class Foo {
private:
    uint32_t x;

public:
    constexpr Foo()
        : x { 3 }
    {}
    // ... and some other constructors
};

Трябва да разпределя масив от тези Foo. Не искам конструкторите по подразбиране на елементите на масива да се изпълняват, защото по-късно така или иначе ще инициализирам изрично всеки елемент. Нещо като това:

Foo foos[20000];

for (int i = 0; i < 20000; ++i) {
    foos[i] = init(i);
}

Има ли начин да получим такъв неинициализиран масив от Foo, като се има предвид, че не ни е позволено да променяме конструктора по подразбиране на Foo в неинициализиращ?

Между другото, ето как бихте създали неинициализиран масив в D:

Foo[20000] foos = void;

...и ето същото в Rust:

let mut foos: [Foo; 20000] = unsafe { std::mem::uninitialized() };

person kmky    schedule 12.03.2015    source източник
comment
@zch: Пазете се от подравняването.   -  person Deduplicator    schedule 12.03.2015
comment
Профилирал ли си, че извикванията на конструктора по подразбиране са причинили проблем с производителността? (Предполагам, че затова се интересувате от това)   -  person Neil Kirk    schedule 12.03.2015
comment
@zch, което нарушава стриктното псевдоним, за съжаление   -  person M.M    schedule 12.03.2015
comment
@NeilKirk: Не, всъщност нямам конкретен проблем с това. Просто ми е интересно да науча как да програмирам подобни неща без ненужни разходи (все пак това е C++:).   -  person kmky    schedule 12.03.2015
comment
Всъщност cppreference има пример, който се занимава с подобен проблем en.cppreference.com/w/cpp /types/aligned_storage   -  person zch    schedule 12.03.2015
comment
@MattMcNabb, char няма строги проблеми с псевдонимите. Въпреки това може да се нуждае от поставяне new.   -  person zch    schedule 12.03.2015
comment
@zch: Наистина не съм сигурен, но мисля, че вашият пример може да причини недефинирано поведение поради някои правила на C++, които се наричат ​​стриктно псевдоним.   -  person kmky    schedule 12.03.2015
comment
@zch char може да се използва за псевдоним на други типове, но друг тип не може да се използва за псевдоним char   -  person M.M    schedule 12.03.2015
comment
@kmky C++ има капсулиране; ако дефинирате класа си така, че единственият начин да го конструирате е да инициализирате стойност на 3, тогава няма друг начин да го конструирате. (Звучи тавтологично, защото е!). Можете да разпределите памет, която все още не съдържа обекти (vector::reserve работи по този начин).   -  person M.M    schedule 12.03.2015
comment
@MattMcNabb. Добре, тогава мисля, че разположението new се справя с това. Тогава вашата програма изобщо не се опитва да получи достъп до съхранената стойност на обект чрез glvalue, докато обект от подходящ тип не бъде конструиран с new. В общи линии това би направило всяко използване на std::aligned_storage.   -  person zch    schedule 12.03.2015
comment
@MattMcNabb: Да го капсулирам така не беше моето намерение. Просто не съм писал други конструктори, защото не смятах, че имат значение. По всякакъв начин класът Foo може да има например следния неинициализиращ конструктор по подразбиране: Foo(Uninitialized) {} и ще има помощно enum, което да го извика: enum Uninitialized { uninit };. Не виждам как този вид отпускане на капсулирането ще помогне за разрешаването на проблема ми.   -  person kmky    schedule 12.03.2015
comment
Предложението на Re zch - съгласен съм, че проблемите с псевдонимите могат да възникнат, ако паметта е променена чрез char*s в arr и чрез Foo* като foos - ако само последното се използва за стартиране/достъп/модифициране на паметта, има ли все още потенциален проблем? не бих рискувал...   -  person Tony Delroy    schedule 12.03.2015
comment
@MattMcNabb: std::array няма конструктори.   -  person kmky    schedule 12.03.2015
comment
Псевдонимът на @TonyD говори за достъп, който включва както четене, така и запис. Вижте 3.10/10. Масив от unsigned char е обект, чийто динамичен тип е array of unsigned char.   -  person M.M    schedule 12.03.2015
comment
@kmky d'oh. Малка слабост на езика е, че масивите в стил C не могат да получат инициализатор, който да се прилага към всички елементи. Можете да напишете Foo foos[20000] = { uninit, uninit, uninit, uninit, и т.н. - или копиране-поставяне, или използване на друга програма за генериране на източника вместо вас. Друг вариант би бил да извлечете от Foo и да предоставите конструктор по подразбиране, който прави това, което искате; и след това имате вашия масив от производния клас (който може да бъде нарязан до Foo, ако е необходимо).   -  person M.M    schedule 12.03.2015
comment
@zch: Мисля, че примерът в предоставената от вас връзка прави точно това Искам да правя. Той разпределя в стека без инициализиране и след това поставя, за да инициализира елементи. Благодаря.   -  person kmky    schedule 12.03.2015
comment
@MattMcNabb: благодаря за препратката към 3.10/10.   -  person Tony Delroy    schedule 12.03.2015


Отговори (3)


Ако използвате C++11, можете да използвате std::vector и emplace_back()

vector<Foo> foos;
for(int i = 0; i < 20000; ++i)
    foos.emplace_back( /* arguments here */);
person yizzlez    schedule 12.03.2015
comment
Това работи, но ако целта на използването на масив в стил C и избягването на използването на конструктори по подразбиране е производителност (защо иначе?), използването на вектор може да е по-бавно. - person Neil Kirk; 12.03.2015
comment
@NeilKirk профил, преди да приемем... Векторът може първоначално да reserve(20000);, за да избегне ненужни разпределения. Както и да е, не виждам по-добър вариант от този. - person M.M; 12.03.2015
comment
@MattMcNabb Съгласен съм с вас, но като се има предвид контекста на въпроса, предположих, че той търси микрооптимизации, предвид вече известно профилиране. Освен това стандартът позволява ли на компилатора да прави това? - person Neil Kirk; 12.03.2015
comment
вектор с резерв трябва да бъде оптимален. той грабва необработени байтове с помощта на разпределител по подразбиране и конструира елементи с placement-new според изискванията. - person M.M; 12.03.2015
comment
Компилаторът може да направи всичко, което не променя наблюдаваното поведение на програмите, въпреки че според моя опит те не биха направили reserve без вие специално да го кодирате - person M.M; 12.03.2015
comment
Ами ако искам да разпределя масива върху стека вместо купчината. Възможно ли е да се направи същия вид магия, която std::vector прави под капака (с reserve и emplace_back) с разпределен в стека петно ​​от неинициализирана памет? - person kmky; 12.03.2015
comment
@MattMcNabb Това е, което казвам. Никога не съм забелязал компилатора ми да резервира, без аз да му кажа. - person Neil Kirk; 12.03.2015
comment
@kmky Трябва да използвате стека само ако имате проблем с производителността, но вие казахте, че не е така. - person Neil Kirk; 12.03.2015
comment
@NeilKirk: Нямам конкретен проблем. Целта ми е да се науча да правя това само в случай, че някога срещна проблем с производителността, който може да бъде облекчен, като не инициализирам всеки елемент два пъти. - person kmky; 12.03.2015
comment
@kmky Знам, че не е перфектно и може да ви изнерви, но никога не съм срещал проблем с производителността, свързан с конструктори по подразбиране в масив, който задава цяло число. Не бих се тревожил много за това :) - person Neil Kirk; 12.03.2015

Това, което може би търсите е std::get_temporary_buffer:

int main()
{
  size_t n = 20000;
  auto buf = std::get_temporary_buffer<Foo>(n);
  if (buf.second<n) {
    std::cerr << "Couldn't allocate enough memory\n";
    return EXIT_FAILURE;
  }

  // ...

  std::raw_storage_iterator<Foo*,Foo> iter(buf.first);
  for (int i = 0; i < n; ++i) {
    *iter++ = Foo();
  }

  // ...

  std::return_temporary_buffer(buf.first);
}
person Vaughn Cato    schedule 12.03.2015
comment
Определя се от внедряването колко ефективни са тези временни буфери. Проверих в едно от Visual Studios и просто тайно се извика new. - person Neil Kirk; 12.03.2015

Може би това отговаря по-точно на поставения въпрос?

#include <type_traits>

class Foo {
private:
    uint32_t x;

public:
    constexpr Foo()
        : x { 3 }
    {}

    constexpr Foo(uint32_t n)
        : x { n * n }
    {}
};

    // ...and then in some function:

    typename std::aligned_storage<sizeof(Foo), alignof(Foo)>::type foos[20000];

    for (int i = 0; i < 20000; ++i) {
        new (foos + i) Foo(i);
    }

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

Въпрос: Мога ли тогава да получа достъп до тези Foo по този начин:

    Foo* ptr = reinterpret_cast<Foo*>(foos);
    ptr[50] = Foo();
person kmky    schedule 12.03.2015
comment
Обектите могат да бъдат конструирани само чрез техния конструктор, това не е недостатък толкова, колкото работата по предназначение. Вашият код за достъп изглежда правилен, стига да го направите след строителния цикъл, разбира се. - person M.M; 12.03.2015