Как вызвать конструктор объектов, содержащийся в std::vector?

Когда я создаю объект std::vector, конструктор этих объектов не всегда вызывается.

#include <iostream>
#include <vector>
using namespace std;

struct C {
    int id;
    static int n;
    C() { id = n++; }   // not called
//  C() { id = 3; }     // ok, called
};

int C::n = 0;


int main()
{
    vector<C> vc;

    vc.resize(10);

    cout << "C::n = " << C::n << endl;

    for(int i = 0; i < vc.size(); ++i)
        cout << i << ": " << vc[i].id << endl;  
}

Это результат, который я получаю:

C::n = 1
0: 0
1: 0
2: 0
...

Вот что мне хотелось бы:

C::n = 10
0: 0
1: 1
2: 2
...

В этом примере я вынужден изменить размер вектора и затем инициализировать его элементы "вручную"?
Может ли причина быть в том, что элементы вектора не инициализируются упорядоченным образом, от первого до последнего, и поэтому я не могу получить детерминированное поведение?

То, что я хотел бы сделать, состоит в том, чтобы легко подсчитать количество объектов, созданных в программе, в разных контейнерах, в разных точках кода и дать каждому из них один идентификатор.

Спасибо в!

Ответ 1

Причина в том, что vector:: resize вставляет копии, вызывая автоматически предоставленный конструктор копирования, а не конструкторы, которые вы определили в своем пример.

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

struct C {
//....
C(const C& other) {
    id = n++;
    // copy other data members
}
//....
};

Из-за того, что работает вектор:: resize (он имеет второй необязательный аргумент, используемый как 'prototype' для создаваемых копий, со значением по умолчанию в вашем случае C()), это создает 11 объектов в ваш пример ('prototype' и 10 его копий).

Изменить (чтобы включить некоторые полезные советы во многие комментарии):

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

  • Этот метод добавляет затраты на обслуживание и степень риска. Вы должны помнить об изменении своего конструктора копий при добавлении или удалении переменных-членов класса. Вам не нужно это делать, если вы полагаетесь на конструктор копии по умолчанию. Один из способов борьбы с этой проблемой состоит в том, чтобы инкапсулировать счетчик в другом классе (как это), что также, возможно, является лучшим дизайном OO, но тогда, конечно, вам также нужно поддерживать в виду многие проблемы, которые могут возникать с множественным наследованием.

  • Это может усложнить понимание другими людьми, потому что копия больше не то, что ожидало бы большинство людей. Аналогичным образом, другой код, который имеет отношение к вашим классам (включая стандартные контейнеры), может быть неправильным. Один из способов борьбы с этим - определить метод operator== для вашего класса (и его можно утверждать, что это хорошая идея при переопределении конструктора копирования, даже если вы не используйте метод), чтобы он был концептуально "звук", а также как своего рода внутренняя документация. Если ваш класс получает много пользы, вы, скорее всего, также получите operator=, чтобы вы могли поддерживать разделение вашего автоматически генерируемого идентификатора экземпляра от назначений членов класса, которые должны выполняться под этим оператором. И так далее;)

  • Он может устранить всю проблему "разных значений идентификатора для копий", если у вас достаточно контроля над программой для использования динамически создаваемых экземпляров (с помощью новых) и использования указателей для этих внутри контейнеров. Это означает, что вам нужно "инициализировать элементы" вручную "до некоторой степени, но не так много работы, чтобы написать функцию, которая возвращает вам вектор, полный указателей, на новые, инициализированные экземпляры. Если вы постоянно используете указатели при использовании стандартных контейнеров, вам не придется беспокоиться о стандартных контейнерах, создающих все экземпляры" под обложками".

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

Ответ 2

конструктор этих объектов не всегда называется.

Да, это так, но это не тот конструктор, о котором вы думаете. Функция-член resize() фактически объявляется следующим образом:

void resize(size_type sz, T c = T());

Второй параметр - это объект для копирования в каждый из вновь вставленных элементов вектора. Если вы опустите второй параметр, он по умолчанию создает объект типа T, затем копирует этот объект в каждый из новых элементов.

В вашем коде создается временная C и вызывается конструктор по умолчанию; id устанавливается в 0. Неявно объявленный конструктор копирования затем называется десять раз (чтобы вставить десять элементов в вектор), и все элементы в векторе имеют одинаковый идентификатор.

[Примечание для тех, кто заинтересован: в С++ 03 второй параметр resize() (C) берется значением; в С++ 0x берется ссылка const lvalue (см. LWG Defect 679)].

В этом примере я вынужден изменить размер вектора и затем инициализировать его элементы "вручную"?

Вы можете (и, вероятно, должны) вставлять элементы в вектор отдельно, например,

std::vector<C> vc;
for (unsigned i(0); i < 10; ++i)
    vc.push_back(C());

Ответ 3

Вектор использует конструктор копирования, который С++ генерирует для вас, не спрашивая. Один экземпляр "C" создается, остальные копируются из прототипа.

Ответ 4

@James: скажем, что я должен уметь различать каждый объект, даже если более одного (временно) имеют одинаковое значение. Его адрес не является чем-то, на что я бы так поверил, из-за перераспределения векторов. Кроме того, различные объекты могут находиться в разных контейнерах. Являются ли проблемы, о которых вы говорите, только что связанные с последующими соглашениями, или могут быть реальные технические проблемы с таким кодом? Тест, который я сделал, хорошо работает.
Вот что я имею в виду:

#include <iostream>
#include <vector>
#include <deque>
using namespace std;

struct C {
    int id;
    static int n;
    int data;

    C() {               // not called from vector
        id = n++;
        data = 123;
    }

    C(const C& other) {
        id = n++;
        data = other.data;
    }

    bool operator== (const C& other) const {
        if(data == other.data)      // ignore id
            return true;
        return false;
    }
};

int C::n = 0;


int main()
{
    vector<C> vc;
    deque<C> dc;

    vc.resize(10);

    dc.resize(8);

    cout << "C::n = " << C::n << endl;

    for(int i = 0; i < vc.size(); ++i)
        cout << "[vector] " << i << ": " << vc[i].id << ";  data = " << vc[i].data << endl;

    for(int i = 0; i < dc.size(); ++i)
        cout << "[deque] " << i << ": " << dc[i].id << ";  data = " << dc[i].data << endl;
}

Вывод:

C::n = 20
[vector] 0: 1;  data = 123
[vector] 1: 2;  data = 123
[vector] 2: 3;  data = 123
[vector] 3: 4;  data = 123
[vector] 4: 5;  data = 123
[vector] 5: 6;  data = 123
[vector] 6: 7;  data = 123
[vector] 7: 8;  data = 123
[vector] 8: 9;  data = 123
[vector] 9: 10;  data = 123
[deque] 0: 12;  data = 123
[deque] 1: 13;  data = 123
[deque] 2: 14;  data = 123
[deque] 3: 15;  data = 123
[deque] 4: 16;  data = 123
[deque] 5: 17;  data = 123
[deque] 6: 18;  data = 123
[deque] 7: 19;  data = 123