Как вызвать конструктор объектов, содержащихся в 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
...

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

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

Спасибо!


person Pietro    schedule 17.07.2010    source источник
comment
Не принимайте ответ, который у вас есть. Это взлом, который делает ваш код беспорядочным и непригодным для использования. Копирующий конструктор должен копировать.   -  person GManNickG    schedule 17.07.2010
comment
Я добавил пример того, как вы можете получить желаемые результаты в этом случае. Тем не менее, вы должны расширить то, чего вы хотите достичь этим. Объекты типа создаются и уничтожаются часто (например, когда происходит перераспределение вектора, все объекты в векторе копируются).   -  person James McNellis    schedule 17.07.2010
comment
Если вы осветите свои цели, мы будем рады предоставить вам безопасное рабочее решение. Но в настоящее время ваши цели противоречивы: вы не можете различать каждый объект, помещая эти объекты в стандартные контейнеры.   -  person GManNickG    schedule 18.07.2010


Ответы (4)


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

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

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

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

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

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

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

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

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

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

person sje397    schedule 17.07.2010
comment
Так не пойдет. Копирующий конструктор должен копировать; в противном случае класс нельзя использовать в стандартном контейнере. Мне бы надоело создавать конструктор-копию, который на самом деле ничего не копирует. - person GManNickG; 17.07.2010
comment
GMan: предположим, что у класса есть некоторые данные, которые необходимо скопировать, и переменная-член, которая должна быть уникальной (идентификатор). Разве в этом случае не правильно написать конструктор копирования, который копирует только то, что он может, и присваивает новое уникальное значение идентификатору? Если это плохой способ продолжить, есть ли альтернатива? - person Pietro; 18.07.2010
comment
@Pietro: Как правило, после вызова конструктора копирования копия должна быть такой же, как оригинал (т.е. original == copy должен быть истинным). В противном случае вы можете столкнуться с причудливыми проблемами, которые будет сложно отлаживать или ваш код будет трудно понять. Редко, когда каждому объекту типа нужен абсолютно уникальный идентификатор (и если это так, часто для этой цели можно использовать его адрес). В каком варианте использования требуется этот уникальный идентификатор? - person James McNellis; 18.07.2010
comment
@James: Я добавил отдельный ответ на ваш комментарий. Спасибо - person Pietro; 18.07.2010
comment
@GMan - класс может (и есть в этом примере) использоваться со стандартными контейнерами. Я понимаю вашу точку зрения, что обычно это не лучшая идея, но если он действительно хочет подсчитать количество раз, когда класс был создан, как он сказал в вопросе, то я думаю, что называть это `` взломом '' немного жесткий. - person sje397; 18.07.2010
comment
@sje: Нет, его нельзя использовать в стандартном контейнере. Прочтите стандарт, а именно 20.1.3. Чтобы C можно было использовать в стандартном контейнере, C(x) должно быть равно x. То есть копия C, созданная с помощью x , должна быть идентична x. В этом отношении ваш код не работает, и поэтому его нельзя использовать в стандартном контейнере. Тот факт, что он работает над вашей реализацией в этом крошечном тесте, не делает его жизнеспособным решением, это нестандартное решение. Это взлом. Чтобы заставить его работать, вам нужно переопределить operator= и убедиться, что id не имеет ничего общего с двумя идентичными вещами. - person GManNickG; 18.07.2010
comment
@GMan - Никто другой пока что не предоставил бедняге другого способа подсчета каждого экземпляра его класса, что он и хочет. Почему бы вам не опубликовать «правильный» ответ? - person sje397; 18.07.2010
comment
@sje: Мы ответили на заглавный вопрос. Он должен задать вопрос, как я могу безопасно отслеживать экземпляры объектов, чтобы получить ответ. :) Если ОП освещает свою цель, буду рад дать ответ. - person GManNickG; 18.07.2010
comment
@GMan: Я переформулирую свой вопрос: как я могу безопасно отслеживать экземпляры объектов? Спасибо. - person Pietro; 19.07.2010
comment
@Pietro: Самое простое решение - сделать их не копируемыми и использовать контейнеры с указателями. - person James McNellis; 19.07.2010
comment
Я пытался задать этот вопрос здесь. - person sje397; 19.07.2010
comment
@GMan @James @Pietro: Я отредактировал, чтобы попытаться обобщить основные моменты в комментариях. - person sje397; 19.07.2010
comment
@sje: +1 за вопросы для исследования и еще один за добавление резюме этих вопросов к вашему ответу. Ergo, я удалил свой голос против и сделал его положительным. Но я все еще с Джеймсом в этом вопросе. :) - person GManNickG; 19.07.2010
comment
@sje: +1 фактически по тем же причинам, что и GMan. - person James McNellis; 21.07.2010

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

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

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

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

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

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

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

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

std::vector<C> vc;
for (unsigned i(0); i < 10; ++i)
    vc.push_back(C());
person James McNellis    schedule 17.07.2010

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

person Community    schedule 17.07.2010

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

#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
person Pietro    schedule 18.07.2010
comment
@Pietro - во-первых, извините за драму, которую, похоже, вызвало мое предложение :) Вы правы в том, что адрес не полностью заслуживает доверия, как если бы один экземпляр был уничтожен, а другой создан, есть шанс, что второй может иметь тот же адрес, что и первый. Мне кажется, что переопределение конструктора копирования не является неподходящим методом для вашей ситуации. Обычно, как уже отмечалось, это не лучшая идея, но из каждого правила есть исключения;) - person sje397; 18.07.2010
comment
@sje: Прекрати давать советы, ты ошибаешься. Переопределение конструктора копирования - это хитрость. Это плохой дизайн и работает только в этом маленьком тесте, нарушая практичность класса в любом другом случае. @Pietro: Не не этого делать, это ужасный код. Код sje делает ваш класс непригодным для использования в стандартных контейнерах. - person GManNickG; 18.07.2010
comment
Я должен уметь различать каждый объект, даже если несколько объектов могут (временно) иметь одно и то же значение. Здесь вы должны определить свое понятие временного и постоянного. Вы можете использовать увеличивающийся идентификатор для идентификации объектов, но тогда либо объекты не должны быть копируемыми, либо копии должны иметь один и тот же идентификатор. Если у копии другой идентификатор, то это не копия. - person James McNellis; 18.07.2010
comment
@GMan: Нет. Мне нравится давать советы (и, кстати, первое предложение в вашем последнем комментарии грамматически неверно). Я доверяю процессу. Является ли дизайн «плохим» или нет, субъективно и зависит не только от проблемы, но и от альтернативных вариантов. Также обратите внимание, что я никогда не предлагал ему не переопределять другие операторы, чтобы его класс соответствовал стандартным требованиям контейнера. - person sje397; 19.07.2010
comment
@James: Он хочет подсчитать экземпляры, в том числе созданные как копии стандартными контейнерами. И его «id» - это «идентификатор экземпляра», поэтому никакие два экземпляра не могут иметь одинаковое значение для id. Таким образом, высказывание «либо объекты не должны копироваться, либо копии должны иметь одинаковый идентификатор» - это просто еще один способ сказать «у вашей проблемы нет решения». - person sje397; 19.07.2010
comment
@ sje397: Решение состоит в том, чтобы сделать класс не копируемым; если нужен контейнер типа, то можно использовать контейнер указателей на этот тип. - person James McNellis; 19.07.2010
comment
@sje: Но ты тоже этого не делал, что не то же самое. Переопределение конструктора копирования без проверки того, что T(x) == x удерживается, является неправильным ответом, что делает класс непригодным для использования. И я весь день буду обсуждать вещи из башни из слоновой кости, но, в конце концов, класс просто невозможно использовать в стандартном контейнере, и предлагать использовать такой неиспользуемый класс - плохо. И нет, @James прав: класс не должен допускать повторения. Нет концептуального смысла в том, чтобы различать каждый класс, в то же время позволяя их копировать. Если копируется один, то никогда не выделяется класс. - person GManNickG; 19.07.2010
comment
@GMan: Я этого не делал, потому что 1) в его исходном классе не было других данных и 2) также не было operator==, поэтому T(x) == x не выполняется независимо от добавления переопределения конструктора копирования. Я согласен с Джеймсом в том, что контейнер указателей, вероятно, является лучшим способом устранить неоднозначность всей проблемы «идентичности экземпляра», но OP спросил: «Я вынужден ... инициализировать его элементы вручную?» чего, строго говоря, не является. Я думаю, имеет ли это концептуальный смысл, зависит от концепции, которую представляет id. Я думаю об этом больше как об «аспекте» за пределами состояния экземпляра. - person sje397; 19.07.2010
comment
@ sje397: Вы правы: id - это «аспект» за пределами состояния экземпляра, т.е. это «специальная» переменная-член. Два объекта имеют одинаковое значение, если значения их переменных-членов (кроме идентификатора) одинаковы. Если даже идентификатор такой же, это означает, что мы сравниваем объект сам с собой. Я изменил свой код, чтобы отразить это. - person Pietro; 19.07.2010