[C++] Зачем оператор присваивания в этом примере?

Realist

  
#include <vector>
struct Struct {
     const int val;
     Struct : val(2) {}
};
int main
{
     std::vector<Struct> array;
     array.push_back(Struct;
     return 0;
}

Реализация push_back требует оператора копирования, который не может быть сгенерирован из-за константности поля val. Так обстоят дела на
 
  
g++ (GCC) 4.1.2 20060715 (prerelease) (Debian 4.1.1-9)

В Visual Studio 2005 код тоже не компилируется.
Почему так? Это техническая недоработка в реализации STL или концептуально оператор присваивания нужен?
Моя логика: если в векторе есть место, то push_back должна _создавать_ новый элемент в конце. Если места нет, то весь массив переезжает на _новое_ место, то есть опять конструктор (не оператор) компирования делает всю работу.
Пойду RTFM, пока вы думаете.
ЗЫ На самом деле оператор присваивания

Werdna

конструктор, во-первых, а не оператор, копирования по умолчанию — это поэлоементное копирование.
Я что-то не знаю как компилятор должен копировать const int.
Вообще, бредовый пример. Не понимаю, зачем там const?

Werdna

только сейчас врубился, что там у тебя вектор как раз конструктора требует, или оператора присваивания. Всё зависит от реализации!

margadon

а как ты хочешь чтобы push_back работал? он должен аллокейтить элемент во внутреннем представлении и ему присвоить передаваемый параметр... палюбому нужно копирование.

ppplva

он должен аллокейтить элемент во внутреннем представлении и ему присвоить передаваемый параметр
Для этого и существуют конструкторы копирования.
Непонятно, зачем нужен оператор.

margadon

здесь была лажа

ppplva

Почему не в лоб ?
realloc
new(addr) Type(obj)
Дефолтный конструктор нужен, но в этой опреции можно обойтись и без него.

margadon

дадада, чё-то я поддался всеобщей мании болтать вместо того, чтобы проверить

Realist

Встречал ли ты реализацию, где этот пример работает?
Да, пример упрощен и не решает никакую конкретную задачу.

Werdna

я мало реализаций встречал, только gcc и stlport

smit1

>Встречал ли ты реализацию, где этот пример работает?
Что самому мешает сделать?

erotic

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

tamusyav

Гым, забавно. Ботание исходников из VS2005 дало следующее:
1. push_back реализован через insert в конец.
2. Соответственно, insert используется универсальный; там рассматриваются различные случаи (есть/нет места и т. п. причем работает ветка, отвечающая за вставку элементов в случае, когда количество вставляемых элементов больше сдвигаемого "хвоста".
3. Поскольку функция универсальная, то она не рассматривает отдельно случай, когда сдвигать ничего не надо (а нужно только вставлять новые элементы в конец). Соответственно, алгоритм такой:
1) двигаем "хвост" на новое место (через конструктор копирования);
2) через конструктор копирования забиваем пространство от "старого" конца до начала "хвоста" (стоящего уже на новом месте);
3) через оператор копирования заменяем позиции, которые раньше занимал "хвост".
Компилятор не додумывается, что при вставке элементов в конец последний этап выполняться, фактически, не будет, и поэтому ругается.

erotic

2) через конструктор копирования забиваем пространство от "старого" конца до начала "хвоста" (стоящего уже на новом месте);
Т.е. фактически, то место, куда надо вставить новые элементы? И чем он их забивает? Почему сначала забивает чем-то, а потом копирует нужные элементы, вместо инициализации сразу новыми элементами?

erotic

А еще, интересно - почему можно делать так:
 	std::vector<const int> zz;
zz.push_back(10);
zz[0] = 20;

Во-первых, почему для const int существует оператор присваивания, а во-вторых, почему возвращаемая оператором [] ссылка тоже не константная...

tamusyav

Под "забивает" я понимал именно то, что это пространство инициализируется теми значениями, которые указал "пользователь". Но повторюсь: это относится только к создаваемым элементам. В общем случае вставки элементов есть уже созданные элементы, вместо которых нужно положить то, что указано в аргументе функции. Вот для них-то и нужен оператор копирования. Он не вызывается при использовании push_back, но на стадии компиляции это неизвестно (точнее, компилятор теоретически может до этого догадаться, но на практике происходит иначе).

tamusyav

Похоже, что vector<const int> - это, в сущности, то же самое, что и vector<int>...
Кусок из <xmemory>

// TEMPLATE CLASS _Allocator_base
template<class _Ty>
struct _Allocator_base
{ // base class for generic allocators
typedef _Ty value_type;
};
// TEMPLATE CLASS _Allocator_base<const _Ty>
template<class _Ty>
struct _Allocator_base<const _Ty>
{ // base class for generic allocators for const _Ty
typedef _Ty value_type;
};
// TEMPLATE CLASS allocator
template<class _Ty>
class allocator
: public _Allocator_base<_Ty>
{ // generic allocator for objects of class _Ty
public:
typedef _Allocator_base<_Ty> _Mybase;
typedef typename _Mybase::value_type value_type;
typedef value_type _FARQ *pointer;
typedef value_type _FARQ& reference;
typedef const value_type _FARQ *const_pointer;
typedef const value_type _FARQ& const_reference;
//...

То есть для векторов константных типов аллокаторы такие же, как и для неконстантных. Содержимое вектора хранится в массиве с типом _Alloc::pointer, где _Alloc по умолчанию в данном случае - allocator<const int>. Везде, где используется тип значения, указывается value_type или производные от него, то есть разницы между аллокаторами (а, следовательно, и векторами) для константных и неконстантных типов нет.
Если второй класс удалить, то возникают большие проблемы, так как многие функции имеют две реализации (для константных ссылок и для неконстантных): наличие еще одного const делает заголовки обеих реализаций одинаковыми и приводит к конфликту.

Realist

Да, ты прав. Я уже подумал, что наверняка есть общее требование на элементы контейнера без уточнения, какие требования должны быть выполнены для каких операций функций. Откуда твоя цитата?
Я вот что нашел в стандарте (ISO/IEC 14882:1998(E

23.1 Container requirements
3 The type of objects stored in these components must meet the requirements of CopyConstructible types (20.1.3 and the additional requirements of Assignable types.
4 In Table 64, T is the type used to instantiate the container, t is a value of T, and u is a value of (possibly const) T.
Table 64---Assignable requirements
expression: t=u
retirn type: T&
post-condition: t is equivalent to u
То есть то, что пример не компилируется, соответствует стандарту.
Можно обсудить, почему стандарт не предписывает уменьшить требования к элементам в случае, если я не хочу использовать всю функциональность STL.
Более интересная на мой взгляд вешь в том, что замена vector на list согласно стандарту не решает проблему, хотя реально пример начинает компилироваться.

Realist

Ботание STL от g++ привело к похожей картине:
1. Если место в конце есть, создаем новый элемент в конце (конструктор копирования).
2. Если места нет — вызываем внутренную функцию вставки одного элемента в конец.
3. функция проверяет, достаточно ли места (его, очевидно, недостаточно). Эта проверка в случае push_back излишня!
4. Если места достаточно, создаем в конце элемент как копию предпоследнего. Все элементы от позиции, на которую надо вставить, вдвигаем вперет через std::copy_backward.
5. На освободившуюся позицию копируем(!) новый элемент. Тут компилятор и ругается.
6. Если места было недостаточно, перераспределяем память. Тут я не ботал.
Итого:
код неоптимален, ибо одно и то же проверяется дважды.
компилятор ругается на код, который реально не работает никогда.

Realist

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

Можешь привести пример.
Рассматриваемый пример не соответствует стандарту, фрагмент которого я привел в
.
Он не компилируется на g++ с сообщением (если опустить весь флуд)
 error: assignment of read-only location 

на
 zz[0] = 20 

kokoc88

Компилятор не додумывается
Очень понравились высказывания, типа этого, в данном топике. И как же на этапе компиляции можно додуматься, что код не будет использоваться?

kokoc88

код неоптимален, ибо одно и то же проверяется дважды
О, мало того, вставляемый элемент как минимум один раз дополнительно копируется конструктором копирования. На случай, если придётся наращивать массив, и ссылка станет неправильной.

Realist

Непонятно, а что такого. Исходя из кода программы компилятор для оптимизации пытается понять, какой код никогда не будет выполнен. В общем случае это невозможно. В простых случаях компилятор с этим справляется. Теоретически у компилятора есть необходимая информация, что пользователь использует только push_back. Та вставляет только в конец, а потому хвост двигать никогда не надо. Реально такие выводы сложны, и компилятор и не делает.

erotic

Цитата из Страуструпа, специальное издание.
Можно обсудить, почему стандарт не предписывает уменьшить требования к элементам в случае, если я не хочу использовать всю функциональность STL.

Думаю, либо сложно в реализации, либо просто язык не позволяет. Хотя... хотя гоню наверное. Можно ж было и не делать push_back через insert.

erotic

3. функция проверяет, достаточно ли места (его, очевидно, недостаточно). Эта проверка в случае push_back излишня!
Что-то я не воткнул. Insert и push_back всегда наращивают размер контейнера, так ведь?
Если вопрос про наличие места в уже выделенной памяти (типа, после использвания reserve, кажется то ее может как быть, так и не быть и при вызове insert, и при push_back.
Т.ч. в чем излишнесть проверки?

Realist

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

Realist

Сорри, ща уточню.
Контейнеры выделяют место с запасом, чтобы каждый раз при добавлении не происходило перераспределениях памяти. reserve может заставить зарезервировать место, но оно и без нее резервируется. Логика такая: если в контейнере есть место, то добавить. Если места нет, то переехать на новое, большее, место и там уже добавить. Реализация push_back проверяет, есть ли место. Если оно есть, то добавляет, если нет, то вызывает более общую функцию вставки (_M_insert_aux передавая два параметра: куда вставить (в конец, end и что вставить (новый элемент, __x). Эта функция проверяет, нужно ли перераспределять память. Если необходимости перераспределения нет, она смещает хвост вправо (хвоста в нашем случае нет, так как мы добавляем в конец) и вставляет элемент на освободившуюся позицию (тут оператор присваивания!). Если есть необходимость перераспределить память, то там что-то дальше ботается, я не стал смотреть. Юмор в том, что функцию вставки _M_insert_aux из push_back мы вызываем только если перераспределение памяти необходимо. Поэтому в данном случае проверка, есть ли место в контейнере, излишня. Из push_back стоит вызывать более частную функцию, которая сразу начинает выделять новую память, перетаскивать туда контейнер и там добавлять в конец.

erotic

Контейнеры выделяют место с запасом, чтобы каждый раз при добавлении не происходило перераспределениях памяти. reserve может заставить зарезервировать место, но оно и без нее резервируется. Логика такая: если в контейнере есть место, то добавить. Если места нет, то переехать на новое, большее, место и там уже добавить. Реализация push_back проверяет, есть ли место. Если оно есть, то добавляет, если нет, то вызывает более общую функцию вставки (_M_insert_aux передавая два параметра: куда вставить (в конец, end и что вставить (новый элемент, __x). Эта функция проверяет, нужно ли перераспределять память. Если необходимости перераспределения нет, она смещает хвост вправо (хвоста в нашем случае нет, так как мы добавляем в конец) и вставляет элемент на освободившуюся позицию (тут оператор присваивания!). Если есть необходимость перераспределить память, то там что-то дальше ботается, я не стал смотреть. Юмор в том, что функцию вставки _M_insert_aux из push_back мы вызываем только если перераспределение памяти необходимо. Поэтому в данном случае проверка, есть ли место в контейнере, излишня. Из push_back стоит вызывать более частную функцию, которая сразу начинает выделять новую память, перетаскивать туда контейнер и там добавлять в конец.
Э, не-не, постой. Зачем в push_back сразу выделять новую память и перетаскивать туда контейнер, если при вызове push_back место в контейнере может еще быть, и достаточно просто скопировать туда новый элемент?!?!
Я, честно говоря, не понимаю, зачем _M_insert_aux проверяет на наличие свободного места, если она вызывается только в том случае, если места нет...
Надо самому посмотреть код, а то что-то непонятки

Realist

Ну а я про что?
/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/bits/stl_vector.h +610

void
push_back(const value_type& __x)
{
// если место еще есть, вставляем
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
{
this->_M_impl.construct(this->_M_impl._M_finish, __x);
++this->_M_impl._M_finish;
}
else
// если зарезервированного места больше нет, вызываем более общую функцию вставки:
_M_insert_aux(end __x);
}

/usr/lib/gcc/i486-linux-gnu/4.1.2/../../../../include/c++/4.1.2/bits/vector.tcc +260
 
template<typename _Tp, typename _Alloc>
void
vector<_Tp, _Alloc>::
_M_insert_aux(iterator __position, const _Tp& __x)
{
// опять проверяем доступность места. в данном случае это лишнее действие
if (this->_M_impl._M_finish != this->_M_impl._M_end_of_storage)
{
this->_M_impl.construct(this->_M_impl._M_finish,
*(this->_M_impl._M_finish - 1;
++this->_M_impl._M_finish;
_Tp __x_copy = __x;
std::copy_backward(__position,
iterator(this->_M_impl._M_finish-2
iterator(this->_M_impl._M_finish-1;
// вот тут программа не компилируется. строчка реально не исполняется
*__position = __x_copy;
}
// код, который реально будет работать (вызовет перераспределение)
else
.....

kokoc88

Непонятно, зачем нужен оператор.
Всё-таки, оператор= нужен при вставке элемента внутрь, когда хватает памяти. Но его, конечно, можно было бы заменить на ~ и КК.

Flack_bfsp

опять проверяем доступность места. в данном случае это лишнее действие
Я правильно понимаю, что этот инсёрт вызывается не только из пуш_бэка? Тогда в чём проблема? Да, при вызове из пуш_бэка провека излишня, при вызовах из других функций может оказаться совсем не излишней. Инсёрт делает свою работу и он не обязан знать, кто его вызывает.

Realist

Правильно понимаешь. Просто так удалить этот код нельзя. Просто можно децл оптимизировать. Меня удивило, что такая очевидная вещь не сделана.
С другой стороны вызов перераспределения памяти — вещь относительно редкая, неприятная и дорогая. Лишний if в ней вряд ли займет заметное время.

Olyalyau

Ботай идеологию вектора. Вектор всегда (при подобающем i) позволяет выполнить vector [i] = ...; Значит ему нужен оператор присваивания базового класса. А уж как там push_back реализован — дело десятое. Вообще, открываешь стандарт C++, там написаны требования к базовым классам для всех контейнеров.
Оставить комментарий
Имя или ник:
Комментарий: