std :: vector объектов и const-корректность

Учтите следующее:

class A {
public:
    const int c; // must not be modified!

    A(int _c)
    :   c(_c)
    {
        // Nothing here
    }

    A(const A& copy)
    : c(copy.c)
    {
        // Nothing here
    }    
};



int main(int argc, char *argv[])
{
    A foo(1337);

    vector<A> vec;
    vec.push_back(foo); // <-- compile error!
    
    return 0;
}

Очевидно, что конструктора копирования недостаточно. Что мне не хватает?

РЕДАКТИРОВАТЬ:
Ofc. Я не могу изменить this->c в operator=() методе, поэтому я не понимаю, как operator=() будет использоваться (хотя этого требует std::vector).


person eisbaw    schedule 08.11.2010    source источник
comment
Что представляет собой ошибку компиляции? Не говорите расплывчато, будьте асом; напишите правильный тестовый пример!   -  person    schedule 08.11.2010
comment
Думаю, у вас есть выбор: либо вы потеряете константу, либо потеряете возможность использовать вектор. Если вы начнете работать над этим и разрешите operator= изменять член const, то теперь у вас есть средства для того, чтобы любой фрагмент кода делал то же самое.   -  person UncleBens    schedule 08.11.2010
comment
const int c; // изменять нельзя! Ваш комментарий выше, означает ли это, что 'c' не должен изменяться чем-то, что использует объекты класса A, или членами самого класса A?   -  person yasouser    schedule 08.11.2010
comment
@anand: c следует устанавливать только в конструкторе. В моем конкретном случае у меня есть указатель на какой-то родительский узел. задача * const parent; этот указатель не должен быть повторно установлен, поэтому изменения не допускаются ни внутри класса, ни за его пределами.   -  person eisbaw    schedule 09.11.2010
comment
@eisbaw: Для меня выбор const int c в качестве члена класса A является основной причиной проблемы. Отсюда мой вопрос. Наличие operator = () и использование const_cast ‹› для отбрасывания const для присвоения значения 'c' звучит как уловка для исправления ошибки компилятора, а не решение реальной проблемы.   -  person yasouser    schedule 09.11.2010
comment
@eisbaw Знаете, вы могли бы просто сделать это вектором умных указателей на ваш класс. Это решит вашу проблему без изменения каких-либо определений.   -  person Conspicuous Compiler    schedule 09.11.2010
comment
Этот код больше не вызывает никаких ошибок, начиная с C ++ 11. Я отметил это в своем ответе здесь: stackoverflow.com/a/63895415/4561887.   -  person Gabriel Staples    schedule 15.09.2020


Ответы (10)


Я не уверен, почему никто этого не сказал, но правильный ответ - отбросить const или сохранить A* в векторе (используя соответствующий интеллектуальный указатель).

Вы можете придать своему классу ужасную семантику, если «копия» вызовет UB или ничего не сделает (и, следовательно, не будет копией), но почему все эти проблемы танцуют вокруг UB и плохого кода? Что вы получите, сделав это const? (Подсказка: ничего.) Проблема носит концептуальный характер: Если у класса есть член константный, то это константный класс. Объекты, которые являются константными, по сути, не могут быть присвоены.

Просто сделайте его неконстантным закрытым и выставьте его значение неизменным. Для пользователей это эквивалентно константе. Это позволяет неявно сгенерированным функциям работать нормально.

person GManNickG    schedule 09.11.2010
comment
Хорошо сказано. Я полагаю, что это был комментарий после const int c, который заставил меня предположить, что другого обходного пути для ситуации @eisbaw не было. Старая пословица звучит верно: зачем усложнять себе жизнь? - person Lee Netherton; 16.11.2010
comment
На самом деле, просто отбросить const не так-то просто. Почему? Поскольку удаление 'const' в полях-членах эффективно заставляет вас отбрасывать 'const' в аргументах конструктора, что, в свою очередь, заставляет вас отбрасывать 'const' в том месте, где вы его используете, и т. Д. (Я думаю, идея должна быть чистый). Решение с указателями std :: vector должно работать (хотя и не очень элегантно). Кстати, в VS2010 вы можете push_back элементы без оператора присваивания в std :: vector. - person Dmitrii Semikin; 01.06.2012
comment
@DmitriiSemikin Удаление константы из поля не влияет на параметры. Если вы назначаете по значению, значение просто копируется в неконстантное поле. класс C {int mX; Имя класса (const int x): mX (x) {}}; - person srm; 19.06.2019
comment
@GManNickG Есть и другая точка зрения. После копирования объект становится другим объектом. Следовательно, постоянство членов не применяется в контексте копирования объектов. К сожалению, теперь мне приходится выбирать между семантикой константных членов и назначением объекту. - person Oleksa; 09.07.2020

Элемент контейнера STL должен быть копируемым и назначаемым 1 (а ваш класс A не является). Тебе нужно перегрузить operator =.

1: §23.1 говорит The type of objects stored in these components must meet the requirements of CopyConstructible types (20.1.3), and the additional requirements of Assignabletypes


ИЗМЕНИТЬ:

Заявление об ограничении ответственности: я не уверен, что следующий фрагмент кода безопасен на 100%. Если он вызывает UB или что-то в этом роде, пожалуйста, дайте мне знать.

A& operator=(const A& assign)
{
    *const_cast<int*> (&c)= assign.c;
    return *this;
}

ИЗМЕНИТЬ 2

Я думаю, что приведенный выше фрагмент кода вызывает Undefined Behavior, потому что попытка отбросить константу квалифицированной переменной const вызывает UB.

person Prasoon Saurav    schedule 08.11.2010
comment
Но оператор = изменяет содержимое * this; как бы вы изменили это- ›c? - person eisbaw; 08.11.2010
comment
@eisbaw: Извините, но вы не можете назначить this->c [потому что это константа] внутри перегруженного operator=. - person Prasoon Saurav; 08.11.2010
comment
@eisbaw: Не могли бы вы написать пустой оператор =, который просто игнорирует все, что вы ему передаете? Это поддерживает требуемый интерфейс, но сохраняет целостность c. - person JTeagle; 08.11.2010
comment
@Prasson: Именно поэтому я и разместил этот вопрос в первую очередь! - person eisbaw; 08.11.2010
comment
@Prasoon: Если * это было объявлено как const (например, A const obj (3); obj = A (42);), то это UB. В противном случае это просто плохая идея. - person ; 08.11.2010
comment
@ Роджер: Да, верно. `§7.1. 5.1 / 4` говорит Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior., но у нас нет здесь const объекта, так что все должно быть в порядке, хотя это плохая идея. - person Prasoon Saurav; 08.11.2010
comment
На самом деле ваше утверждение не совсем верно. vector требует, чтобы элемент был копируемым и назначаемым, но не все контейнеры обеспечивают это (список не обеспечивает возможность присваивания). - person CashCow; 08.11.2010
comment
c объявлен const в class A, поэтому код всегда UB, верно? - person Mark B; 08.11.2010
comment
@Mark B: Да! Позвольте мне добавить это к посту. - person Prasoon Saurav; 08.11.2010
comment
Снято ли это ограничение в C ++ 0x? emplace_back приходит на ум как возможность вставить новый элемент, который не должен меняться (пока есть достаточно места). - person Matthieu M.; 08.11.2010
comment
@Oli: Возможно, но только если вы трактуете operator = как «сделать точную копию». Если рассматривать это как «скопировать все соответствующие части из», тогда это имеет смысл, просто список «соответствующих частей» здесь будет пустым. Если бы operator = всегда должен был быть `` сделать точную копию '', тогда не было бы смысла разрешать его переопределение - это могло бы быть значением по умолчанию каждый раз, потому что компилятор знал бы, как точно дублировать элементы объекта (он мог бы сделать memcpy ()!). - person JTeagle; 09.11.2010
comment
Мне кажется, что это становится допустимым и четко определенным поведением по стандарту, если вы добавляете volatile и делаете переменную члена класса volatile const int c; вместо просто const int c;. Я не могу сказать, что это отличная идея, но я думаю, что тогда то, что вы делаете, становится четко определенным поведением и совершенно обоснованным. В противном случае поведение не определено только потому, что чтения c могут быть кэшированы и / или оптимизированы, поскольку это только const. См. Мой ответ здесь: stackoverflow.com/questions/4136156/. - person Gabriel Staples; 15.09.2020

Вам не хватает оператора присваивания (или оператора присваивания копии), одного из больших три.

person sje397    schedule 08.11.2010

Сохраненный тип должен соответствовать требованиям CopyConstructible и Assignable, что означает, что также необходим оператор =.

person icecrime    schedule 08.11.2010

Вероятно, оператор присваивания. Компилятор обычно генерирует для вас вариант по умолчанию, но эта функция отключена, поскольку ваш класс имеет нетривиальную семантику копирования.

person Frédéric Hamidi    schedule 08.11.2010

Я думаю, что для реализации используемых вами векторных функций в STL требуется оператор присваивания (см. Цитату Prasoon из Стандарта). Однако согласно приведенной ниже цитате, поскольку оператор присваивания в вашем коде неявно определен (поскольку он не определен явно), ваша программа плохо сформирована из-за того, что ваш класс также имеет константный нестатический член данных.

C++03

$ 12.8 / 12 - «Неявно объявленный оператор присваивания копии неявно определяется, когда объекту его типа класса присваивается значение его типа класса или значение типа класса, производного от его типа класса. Программа плохо сформирована, если класс для которых неявно определен оператор присваивания копии, имеет:

- нестатический член данных типа const, или

- нестатический элемент данных ссылочного типа, или

- нестатический член данных типа класса (или его массива) с недоступным оператором присваивания копии, или

- базовый класс с недоступным оператором присваивания копии.

person Chubsdad    schedule 08.11.2010

Обходной путь без const_cast.

A& operator=(const A& right) 
{ 
    if (this == &right) return *this; 
    this->~A();
    new (this) A(right);
    return *this; 
} 
person Alexey Malistov    schedule 08.11.2010
comment
Разве это не просто замена одного типа неопределенного поведения другим? - person TheUndeadFish; 08.11.2010
comment
@ Роджер Пейт. Правильно. Нужен чек. Я исправил. - person Alexey Malistov; 09.11.2010
comment
@TheUndeadFish. Где здесь UB? - person Alexey Malistov; 09.11.2010
comment
@Alexey: UB происходит для производных классов; это просто плохой способ реализовать назначение копии. - person ; 09.11.2010
comment
@ Роджер Пейт. Почему UB происходит для производных классов? Мой оператор просто заменяет базовую часть, а не производный класс. - person Alexey Malistov; 09.11.2010
comment
@Alexey: struct A {виртуальный ~ A (); }; структура B: A {}; (TheUndeadFish мог означать другую форму UB, но я знаю, что она существует.) - person ; 09.11.2010
comment
~A() на самом деле не виртуальный. - person Alexey Malistov; 09.11.2010
comment
@ Алексей Хммм, возможно, я ошибался. Мне показалось, что я видел обсуждение, в котором говорилось о реконструкции такого объекта, как UB. Но сейчас я не могу этого найти, так что, возможно, я неправильно это запомнил. - person TheUndeadFish; 10.11.2010
comment
К вашему сведению читателей сегодня, вот следующий вопрос от @AlexeyMalistov, чтобы проверить, является ли это интересное использование нового размещения (new (this) A(right); // copy-construct a new 'A' object from 'right' directly into address 'this') неопределенным поведением: stackoverflow.com/q/4136156/4561887. - person Gabriel Staples; 15.09.2020

Недавно я столкнулся с той же ситуацией и вместо этого использовал std :: set, потому что для его механизма добавления элемента (вставки) не требуется оператор = (используется оператор ‹), в отличие от векторного механизма (push_back).

Если производительность является проблемой, вы можете попробовать unordered_set или что-то подобное.

person Mohamed Koubaa    schedule 09.03.2015

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

class A {
public:
    const int c; // must not be modified!

    A(int _c)
    ...

    A(const A& copy)
    ...  

    A& operator=(const A& rhs)
    {
        int * p_writable_c = const_cast<int *>(&c);
        *p_writable_c = rhs.c;
        return *this;
    }

};

Специальный шаблон const_cast принимает тип указателя и возвращает его в записываемую форму для таких случаев, как этот.

Следует отметить, что const_cast не всегда безопасно использовать, см. здесь.

person Lee Netherton    schedule 08.11.2010

Я просто хочу указать, что начиная с C ++ 11 и более поздних версий исходный код в вопросе компилируется отлично! Никаких ошибок. Тем не менее, vec.emplace_back() было бы лучше, так как он использует размещение новых внутренне и, следовательно, более эффективно, копирование-конструирование объекта прямо в память в конце вектора, вместо того, чтобы иметь дополнительную промежуточную копию.

состояния cppreference (курсив добавлен):

std::vector<T,Allocator>::emplace_back

Добавляет новый элемент в конец контейнера. Элемент создается с помощью std::allocator_traits::construct, который обычно использует place-new для создания элемента на месте в месте, указанном контейнером.

Вот небольшая демонстрация, показывающая, что и vec.push_back(), и vec.emplace_back() теперь работают нормально.

Запустите его здесь: https://onlinegdb.com/BkFkja6ED.

#include <cstdio>
#include <vector>

class A {
public:
    const int c; // must not be modified!

    A(int _c)
    :   c(_c)
    {
        // Nothing here
    }

    // Copy constructor 
    A(const A& copy)
    : c(copy.c)
    {
        // Nothing here
    }    
};

int main(int argc, char *argv[])
{
    A foo(1337);
    A foo2(999);

    std::vector<A> vec;
    vec.push_back(foo); // works!
    vec.emplace_back(foo2); // also works!
    
    for (size_t i = 0; i < vec.size(); i++)
    {
        printf("vec[%lu].c = %i\n", i, vec[i].c);
    }
    
    return 0;
}

Вывод:

vec[0].c = 1337
vec[1].c = 999
person Gabriel Staples    schedule 15.09.2020