Как я могу избежать std :: vector <> для инициализации всех его элементов? - PullRequest
30 голосов
/ 11 мая 2011

РЕДАКТИРОВАТЬ: Я отредактировал вопрос и его название, чтобы быть более точным.

Учитывая следующий исходный код:

#include <vector>
struct xyz {
    xyz() { } // empty constructor, but the compiler doesn't care
    xyz(const xyz& o): v(o.v) { } 
    xyz& operator=(const xyz& o) { v=o.v; return *this; }
    int v; // <will be initialized to int(), which means 0
};

std::vector<xyz> test() {
    return std::vector<xyz>(1024); // will do a memset() :-(
}

... как можно избежать инициализации памяти, выделенной вектором <>, копиями его первого элемента, что является операцией O (n), которую я предпочел бы пропустить ради скорости, так как по умолчанию конструктор ничего не делает?

Подойдет решение, специфичное для g ++, если не существует универсального решения (но я не смог найти никакого атрибута для этого).

РЕДАКТИРОВАТЬ : сгенерированный код следует (командная строка: arm-elf-g ++ - 4.5 -O3 -S -fno-verbose-asm -o - test.cpp | arm-elf-c ++ фильт | grep -vE '^ [[: space:]] + [. @]. * $')

test():
    mov r3, #0
    stmfd   sp!, {r4, lr}
    mov r4, r0
    str r3, [r0, #0]
    str r3, [r0, #4]
    str r3, [r0, #8]
    mov r0, #4096
    bl  operator new(unsigned long)
    add r1, r0, #4096
    add r2, r0, #4080
    str r0, [r4, #0]
    stmib   r4, {r0, r1}
    add r2, r2, #12
    b       .L4          @
.L8:                     @
    add     r0, r0, #4   @
.L4:                     @
    cmp     r0, #0       @  fill the memory
    movne   r3, #0       @
    strne   r3, [r0, #0] @
    cmp     r0, r2       @
    bne     .L8          @
    str r1, [r4, #4]
    mov r0, r4
    ldmfd   sp!, {r4, pc}

РЕДАКТИРОВАТЬ: Для полноты, вот сборка для x86_64:

.globl test()
test():
LFB450:
    pushq   %rbp
LCFI0:
    movq    %rsp, %rbp
LCFI1:
    pushq   %rbx
LCFI2:
    movq    %rdi, %rbx
    subq    $8, %rsp
LCFI3:
    movq    $0, (%rdi)
    movq    $0, 8(%rdi)
    movq    $0, 16(%rdi)
    movl    $4096, %edi
    call    operator new(unsigned long)
    leaq    4096(%rax), %rcx
    movq    %rax, (%rbx)
    movq    %rax, 8(%rbx)
    leaq    4092(%rax), %rdx
    movq    %rcx, 16(%rbx)
    jmp     L4          @
L8:                     @
    addq    $4, %rax    @
L4:                     @
    testq   %rax, %rax  @ memory-filling loop
    je      L2          @
    movl    $0, (%rax)  @
L2:                     @
    cmpq    %rdx, %rax  @
    jne     L8          @
    movq    %rcx, 8(%rbx)
    movq    %rbx, %rax
    addq    $8, %rsp
    popq    %rbx
    leave
LCFI4:
    ret
LFE450:
EH_frame1:
LSCIE1:
LECIE1:
LSFDE1:
LASFDE1:
LEFDE1:

РЕДАКТИРОВАТЬ: Я думаю, что вывод не использовать std::vector<>, когда вы хотите избежать ненужной инициализации. В итоге я развернул свой собственный шаблонный контейнер, который работает лучше (и имеет специализированные версии для neon и armv7).

Ответы [ 9 ]

12 голосов
/ 11 мая 2011

Инициализация выделенных элементов контролируется аргументом шаблона Allocator. Если вам нужно настроить его, настройте его.Но помните, что это может легко привести к грязному взлому, поэтому используйте его с осторожностью.Например, вот довольно грязное решение.Это позволит избежать инициализации, но, скорее всего, будет хуже по производительности, но ради демонстрации (как говорили люди, это невозможно! ... невозможно - не в словаре программиста C ++!):

template <typename T>
class switch_init_allocator : public std::allocator< T > {
  private:
    bool* should_init;
  public:
    template <typename U>
    struct rebind {
      typedef switch_init_allocator<U> other;
    };

    //provide the required no-throw constructors / destructors:
    switch_init_allocator(bool* aShouldInit = NULL) throw() : std::allocator<T>(), should_init(aShouldInit) { };
    switch_init_allocator(const switch_init_allocator<T>& rhs) throw() : std::allocator<T>(rhs), should_init(rhs.should_init) { };
    template <typename U>
    switch_init_allocator(const switch_init_allocator<U>& rhs, bool* aShouldInit = NULL) throw() : std::allocator<T>(rhs), should_init(aShouldInit) { };
    ~switch_init_allocator() throw() { };

    //import the required typedefs:
    typedef typename std::allocator<T>::value_type value_type;
    typedef typename std::allocator<T>::pointer pointer;
    typedef typename std::allocator<T>::reference reference;
    typedef typename std::allocator<T>::const_pointer const_pointer;
    typedef typename std::allocator<T>::const_reference const_reference;
    typedef typename std::allocator<T>::size_type size_type;
    typedef typename std::allocator<T>::difference_type difference_type;

    //redefine the construct function (hiding the base-class version):
    void construct( pointer p, const_reference cr) {
      if((should_init) && (*should_init))
        new ((void*)p) T ( cr );
      //else, do nothing.
    };
};

template <typename T>
class my_vector : public std::vector<T, switch_init_allocator<T> > {
  public:
    typedef std::vector<T, switch_init_allocator<T> > base_type;
    typedef switch_init_allocator<T> allocator_type;
    typedef std::vector<T, allocator_type > vector_type;
    typedef typename base_type::size_type size_type;
  private:
    bool switch_flag; //the order here is very important!!
    vector_type vec;
  public:  
    my_vector(size_type aCount) : switch_flag(false), vec(aCount, allocator_type(&switch_flag)) { };
    //... and the rest of this wrapper class...
    vector_type& get_vector() { return vec; };
    const vector_type& get_vector() const { return vec; };
    void set_switch(bool value) { switch_flag = value; };
};

class xyz{};

int main(){
  my_vector<xyz> v(1024); //this won't initialize the memory at all.
  v.set_switch(true); //set back to true to turn initialization back on (needed for resizing and such)
}

Конечно, вышесказанное неудобно и не рекомендуется, и, безусловно, не будет лучше, чем фактическое заполнение памяти копиями первого элемента (тем более что использование этой проверки флага будет препятствовать каждому элементу).строительство).Но это путь для изучения при поиске оптимизации размещения и инициализации элементов в контейнере STL, поэтому я хотел показать это.Дело в том, что единственное место, где вы можете внедрить код, который не позволит контейнеру std :: vector вызывать конструктор копирования для инициализации ваших элементов, - это функция конструкции объекта-распределителя вектора.

Кроме того, выможет покончить с «switch» и просто сделать «no-init-allocator», но затем вы также отключите функцию копирования, которая необходима для копирования данных во время изменения размера (что сделает этот векторный класс гораздо менее полезным),

9 голосов
/ 11 мая 2011

Это странный угол vector. Проблема в , а не в том, что ваш элемент инициализируется значением ... дело в том, что случайное содержимое в первом элементе-прототипе копируется во все другие элементы вектора. (Это поведение изменилось в C ++ 11, значение которого инициализирует каждый элемент).

Это (/ было) сделано по уважительной причине: рассмотрим некоторый объект с подсчетом ссылок ... если вы создаете vector, запрашивающий 1000 элементов, инициализированных для такого объекта, вы, очевидно, хотите один объект с подсчетом ссылок 1000, а не 1000 независимых «клонов». Я говорю «очевидно», потому что подсчет ссылки на объект в первую очередь подразумевает, что это очень желательно.

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


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

ПРЕДУПРЕЖДЕНИЕ : не только для этого "решения", но и для всей этой попытки избежать построения по умолчанию ... не делайте этого для типов с важными инвариантами - вы нарушаете инкапсуляцию и может легко иметь vector сам по себе или какую-либо операцию, которую вы пытаетесь вызвать, operator=(), конструкторы копирования и / или деструкторы, где *this / левые и / или правые аргументы не учитывают эти инварианты. Например, избегайте типов значений с указателями, которые ожидаются равными NULL, или с действительными объектами, счетчиками ссылок, дескрипторами ресурсов и т. Д.

#include <iostream>
#include <vector>

struct Uninitialised_Resize
{
    explicit Uninitialised_Resize(int n) : n_(n) { }
    explicit Uninitialised_Resize() { }
    int n_;
};

namespace std
{
    template <>
    template <>
    void vector<int>::assign(Uninitialised_Resize ur, Uninitialised_Resize)
    {
        this->_M_impl._M_finish = this->_M_impl._M_start + ur.n_;

        // note: a simpler alternative (doesn't need "n_") is to set...
        //   this->_M_impl._M_finish = this->_M_impl._M_end_of_storage;
        // ...which means size() will become capacity(), which may be more
        // you reserved() (due to rounding; good) or have data for
        // (bad if you have to track in-use elements elsewhere,
        //  which makes the situation equivalent to just reserve()),
        // but if you can somehow use the extra elements then all's good.
    }
}

int main()
{
    {
        // try to get some non-0 values on heap ready for recycling...
        std::vector<int> x(10000);
        for (int i = 0; i < x.size(); ++i)
            x[i] = i;
    }

    std::vector<int> x;
    x.reserve(10000);
    for (int i = 1; i < x.capacity(); ++i)
        if (x[0] != x[i])
        {
            std::cout << "lucky\n";
            break;
        }
    x.assign(Uninitialised_Resize(1000), Uninitialised_Resize());

    for (int i = 1; i < x.size(); ++i)
        if (x[0] != x[i])
        {
            std::cout << "success [0] " << x[0] << " != [" << i << "] "
                << x[i] << '\n';
            break;
        }
}

Мой вывод:

lucky
success [0] 0 != [1] 1

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

3 голосов
/ 11 мая 2011

Вы упаковываете все свои примитивы в структуру:

struct IntStruct
{
    IntStruct();

    int myInt;
}

с IntStruct (), определенным как пустой конструктор.Таким образом, вы объявляете v как IntStruct v;, поэтому, когда vector из xyzs все значения инициализируются, все, что они делают, это инициализирует значение v, что является недопустимым.

РЕДАКТИРОВАТЬ: Я неправильно понял вопрос.Это то, что вы должны делать, если у вас есть vector примитивных типов, потому что vector определен для инициализации значения при создании элементов с помощью метода resize().Структуры не обязаны инициализировать значения своих членов при построении, хотя эти «неинициализированные» значения все еще могут быть установлены в 0 чем-то другим - эй, они могут быть чем угодно.

1 голос
/ 03 октября 2012

Вы не можете избежать инициализации элементов std :: vector.

По этой причине я использую производный от std :: vector класс.resize() реализовано в этом примере.Вы также должны реализовать конструкторы.

Хотя это не стандартный C ++, а реализация компилятора: - (

#include <vector>

template<typename _Tp, typename _Alloc = std::allocator<_Tp>>
class uvector : public std::vector<_Tp, _Alloc>
{
    typedef std::vector<_Tp, _Alloc> parent;
    using parent::_M_impl;

public:
    using parent::capacity;
    using parent::reserve;
    using parent::size;
    using typename parent::size_type;

    void resize(size_type sz)
    {
        if (sz <= size())
            parent::resize(sz);
        else
        {
            if (sz > capacity()) reserve(sz);
            _M_impl._M_finish = _M_impl._M_start + sz;
        }
    }
};
1 голос
/ 11 мая 2011

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

#include <vector>

template<typename T>
static T create_uninitialized(size_t size, size_t capacity) {
    T v;
#if defined(__GNUC__)
    // Don't say it. I know -_-;
    // Oddly, _M_impl is public in _Vector_base !?
    typedef typename T::value_type     value_type;
    typedef typename T::allocator_type allocator_type;
    typedef std::_Vector_base<value_type, allocator_type> base_type;
    base_type& xb(reinterpret_cast<base_type&>(v));
    value_type* p(new value_type[capacity]);
#if !defined(__EXCEPTIONS)
    size=p?size:0;         // size=0 if p is null
    capacity=p?capacity:0; // capacity=0 if p is null
#endif
    capacity=std::max(size, capacity); // ensure size<=capacity
    xb._M_impl._M_start = p;
    xb._M_impl._M_finish = p+size;
    xb._M_impl._M_end_of_storage = p+capacity;
#else
    // Fallback, for the other compilers
    capacity=std::max(size, capacity);
    v.reserve(capacity);
    v.resize(size);
#endif
    return v;
}

struct xyz {
    // empty default constructor
    xyz() { }
    xyz(const xyz& o): v(o.v) { }
    xyz& operator=(const xyz& o) { v=o.v; return *this; }
    int v;
    typedef std::vector<xyz> vector;
};

// test functions for assembly dump
extern xyz::vector xyz_create() {
    // Create an uninitialized vector of 12 elements, with
    // a capacity to hold 256 elements.
    return create_uninitialized<xyz::vector>(12,256);
}

extern void xyz_fill(xyz::vector& x) {
    // Assign some values for testing
    for (int i(0); i<x.size(); ++i) x[i].v = i;
}

// test
#include <iostream>
int main() {
    xyz::vector x(xyz_create());
    xyz_fill(x);
    // Dump the vector
    for (int i(0); i<x.size(); ++i) std::cerr << x[i].v << "\n";
    return 0;
}

РЕДАКТИРОВАТЬ: понял _Vector_impl был публичным, что упрощает вещи.

РЕДАКТИРОВАТЬ: вот сгенерированная сборка ARM для xyz_create (), скомпилированная с -fno-exceptions (разобрана с использованием фильтра C ++) и без какого-либо цикла инициализации памяти:

xyz_create():
    mov r3, #0
    stmfd   sp!, {r4, lr}
    mov r4, r0
    str r3, [r0, #0]
    str r3, [r0, #4]
    str r3, [r0, #8]
    mov r0, #1024
    bl  operator new[](unsigned long)(PLT)
    cmp r0, #0
    moveq   r3, r0
    movne   r3, #1024
    moveq   r2, r0
    movne   r2, #48
    add r2, r0, r2
    add r3, r0, r3
    stmia   r4, {r0, r2, r3}    @ phole stm
    mov r0, r4
    ldmfd   sp!, {r4, pc}

.. а здесь для x86_64:

xyz_create():
    pushq   %rbp
    movq    %rsp, %rbp
    pushq   %rbx
    movq    %rdi, %rbx
    subq    $8, %rsp
    movq    $0, (%rdi)
    movq    $0, 8(%rdi)
    movq    $0, 16(%rdi)
    movl    $1024, %edi
    call    operator new[](unsigned long)
    cmpq    $1, %rax
    movq    %rax, (%rbx)
    sbbq    %rdx, %rdx
    notq    %rdx
    andl    $1024, %edx
    cmpq    $1, %rax
    sbbq    %rcx, %rcx
    leaq    (%rax,%rdx), %rdx
    notq    %rcx
    andl    $48, %ecx
    movq    %rdx, 16(%rbx)
    leaq    (%rax,%rcx), %rcx
    movq    %rbx, %rax
    movq    %rcx, 8(%rbx)
    addq    $8, %rsp
    popq    %rbx
    leave
    ret
1 голос
/ 11 мая 2011

Я не вижу инициализированной памяти.Конструктор по умолчанию int() ничего не делает, как в C.

Программа:

#include <iostream>
#include <vector>

struct xyz {
    xyz() {}
    xyz(const xyz& o): v(o.v) {} 
    xyz& operator=(const xyz& o) { v=o.v; return *this; }
    int v;
};

std::vector<xyz> test() {
    return std::vector<xyz>(1024);
}

int main()
{
    std::vector<xyz> foo = test();
    for(int i = 0; i < 10; ++i)
    {
        std::cout << i << ": " << foo[i].v << std::endl;
    }
    return 0;
}

Вывод:

$ g++ -o foo foo.cc
$ ./foo 
0: 1606418432
1: 1606418432
2: 1606418432
3: 1606418432
4: 1606418432
5: 1606418432
6: 1606418432
7: 1606418432
8: 1606418432
9: 1606418432

РЕДАКТИРОВАТЬ:

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

Модифицированный пример:

#include <iostream>
#include <vector>
#include <iterator>

struct xyz {
    xyz() {}
    xyz(int init): v(init) {}
    xyz(const xyz& o): v(o.v) {} 
    xyz& operator=(const xyz& o) { v=o.v; return *this; }
    int v;
};

class XYZInitIterator: public std::iterator<std::input_iterator_tag, xyz>
{
public:
                        XYZInitIterator(int init): count(init) {}
                        XYZInitIterator(const XYZInitIterator& iter)
                        : count(iter.count) {}
    XYZInitIterator&    operator=(const XYZInitIterator& iter)
                        { count = iter.count; return *this; }
    value_type          operator*() const { return xyz(count); }
    bool                operator==(const XYZInitIterator& other) const 
                        { return count == other.count; }
    bool                operator!=(const XYZInitIterator& other) const 
                        { return count != other.count; }
    value_type          operator++() { return xyz(++count); }
    value_type          operator++(int) { return xyz(count++); }
private:
    int count;
};

std::vector<xyz> test() {
    XYZInitIterator start(0), end(1024);
    return std::vector<xyz>(start, end);
}

int main()
{
    std::vector<xyz> foo = test();
    for(int i = 0; i < 10; ++i)
    {
        std::cout << std::dec << i << ": " << std::hex << foo[i].v << std::endl;
    }
    return 0;
}

Вывод:

$ g++ -o foo foo.cc
$ ./foo 
0: 0
1: 1
2: 2
3: 3
4: 4
5: 5
6: 6
7: 7
8: 8
9: 9
0 голосов
/ 11 мая 2011

С учетом того, как ваш struct объявлен в данный момент, не существует механизма для инициализации по умолчанию члена int вашей структуры, поэтому вы получаете поведение C по умолчанию, которое является неопределенной инициализацией. Чтобы инициализировать переменную-член int значением инициализации по умолчанию, вам необходимо добавить его в список инициализации конструктора структуры. Например,

struct xyz {
    xyz(): v() { } //initialization list sets the value of int v to 0
    int v;
};

Где-а

struct xyz {
    xyz(): { } //no initialization list, therefore 'v' remains uninitialized
    int v;
};
0 голосов
/ 11 мая 2011

Если вы хотите вектор с зарезервированной только памятью, но без инициализированных элементов, используйте reserve вместо конструктора:

std::vector<xyz> v;
v.reserve(1024);
assert(v.capacity() >= 1024);
assert(v.size() == 0);
0 голосов
/ 11 мая 2011

Мне тоже любопытно. Вы просто хотите, чтобы случайная память была инициализирована?

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...