Подтвердить что ты не робот

Что означает "Assignable"?

С++ 11 устранил требование, чтобы тип значения всех контейнеров был CopyConstructible и Assignable (хотя конкретные операции с контейнерами могут налагать эти требования). Теоретически это должно позволить определить, например, std::deque<const Foo>, что невозможно в С++ 03.

Неожиданно gcc 4.7.2 произвел свою обычную рвоту непонятных ошибок [1], когда я это пробовал, но clang, по крайней мере, сделал ошибку, читаемую и clang с libС++, скомпилировал ее без ошибок.

Теперь, когда два разных компилятора производят разные результаты, это всегда заставляет меня задаться вопросом, какой правильный ответ, и поэтому я искал все ссылки, которые я мог найти для констант/назначаемых/типов значений/контейнеров и т.д. и т.д. Я нашел почти десятилетние ответы на очень похожие вопросы и ответы, некоторые из них здесь, на SO и другие в различных списках рассылки С++, среди других мест, в том числе Buganizer Gnu, все из которых в основном можно обобщить как следующий диалог.

В: Почему я не могу объявить std::vector<const int> (как упрощенный пример)

A: Почему бы вам это сделать? Это бессмысленно.

Q: Ну, это имеет смысл для меня. Почему я не могу это сделать?

A: Поскольку Стандарт требует назначения типов значений.

Q: Но я не планирую назначать их. Я хочу, чтобы они были const после того, как я их создал.

A: Это не так, как это работает. Следующий вопрос!

с мягким лишением:

A2: С++ 11 решил разрешить это. Тебе просто придется подождать. В то же время переосмыслите свой смешной дизайн.

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

В любом случае, я начал думать об ответе: "Стандарт требует, чтобы все типы значений контейнеров были назначаемыми". И, насколько я вижу, теперь, когда я нашел старую копию проекта стандарта С++ 03, это правда; он сделал.

С другой стороны, тип значения std::map - это std::pair<const Key, T>, который не похож на меня как назначаемый. Тем не менее, я снова попробовал std::deque<std::tuple<const Foo>>, и gcc продолжал компилировать его, не моргнув глазом. Поэтому, по крайней мере, у меня есть какое-то решение.

Затем я попробовал распечатать значение std::is_assignable<const Foo, const Foo> и std::is_assignable<std::tuple<const Foo>, const std::tuple<const Foo>>, и оказывается, что первое сообщение считается не назначаемым, как и следовало ожидать, но последнее сообщается как присваиваемое (как clang, так и НКА). Конечно, он не может быть назначен; попытка скомпилировать a = b; отклоняется gcc с жалобой error: assignment of read-only location (это было всего лишь единственное сообщение об ошибке, с которым я столкнулся в этом квесте, что было действительно легко понять). Однако, не пытаясь выполнить задание, оба clang и gcc одинаково счастливы создать экземпляр deque<const>, и код, кажется, работает нормально.

Теперь, если std::tuple<const int> действительно назначается, то я не могу жаловаться на несогласованность в стандарте C++03 - и, действительно, кому это интересно - но я нахожу, что это беспокоит то, что отчеты по двум различным стандартным библиотекам что тип присваивается, если на самом деле попытка присвоить ссылку ему приведет к (очень разумной) ошибке компилятора. Я мог бы в какой-то момент захотеть использовать тест в шаблоне SFINAE и, основываясь на том, что я видел сегодня, выглядит не очень надежным.

Так есть ли кто-нибудь, кто может пролить свет на вопрос (в заголовке): что означает Assignable? И два бонусных вопроса:

1) Действительно ли комитет имел в виду позволить экземпляры контейнеров с типами значений const или имел ли они какой-то другой неприемлемый случай?, и

2) Действительно ли существует значительная разница между константами const Foo и std::tuple<const Foo>?


[1] Для действительно любопытного здесь сообщение об ошибке, созданное gcc при попытке скомпилировать декларацию std::deque<const std::string> (с добавлением нескольких строк и объяснение, если вы прокрутите вниз достаточно далеко):

In file included from /usr/include/x86_64-linux-gnu/c++/4.7/./bits/c++allocator.h:34:0,
                 from /usr/include/c++/4.7/bits/allocator.h:48,
                 from /usr/include/c++/4.7/string:43,
                 from /usr/include/c++/4.7/random:41,
                 from /usr/include/c++/4.7/bits/stl_algo.h:67,
                 from /usr/include/c++/4.7/algorithm:63,
                 from const_stack.cc:1:
/usr/include/c++/4.7/ext/new_allocator.h: In instantiation of ‘class __gnu_cxx::new_allocator<const std::basic_string<char> >’:
/usr/include/c++/4.7/bits/allocator.h:89:11:   required from ‘class std::allocator<const std::basic_string<char> >’
/usr/include/c++/4.7/bits/stl_deque.h:489:61:   required from ‘class std::_Deque_base<const std::basic_string<char>, std::allocator<const std::basic_string<char> > >’
/usr/include/c++/4.7/bits/stl_deque.h:728:11:   required from ‘class std::deque<const std::basic_string<char> >’
const_stack.cc:112:27:   required from here
/usr/include/c++/4.7/ext/new_allocator.h:83:7:
  error: ‘const _Tp* __gnu_cxx::new_allocator< <template-parameter-1-1> >::address(
  __gnu_cxx::new_allocator< <template-parameter-1-1> >::const_reference) const [
  with _Tp = const std::basic_string<char>;
  __gnu_cxx::new_allocator< <template-parameter-1-1> >::const_pointer =
    const std::basic_string<char>*;
  __gnu_cxx::new_allocator< <template-parameter-1-1> >::const_reference =
    const std::basic_string<char>&]’ cannot be overloaded
/usr/include/c++/4.7/ext/new_allocator.h:79:7:
  error: with ‘_Tp* __gnu_cxx::new_allocator< <template-parameter-1-1> >::address(
  __gnu_cxx::new_allocator< <template-parameter-1-1> >::reference) const [
  with _Tp = const std::basic_string<char>;
  __gnu_cxx::new_allocator< <template-parameter-1-1> >::pointer = const std::basic_string<char>*;
  __gnu_cxx::new_allocator< <template-parameter-1-1> >::reference = const std::basic_string<char>&]’

Так что здесь происходит то, что стандарт (& sect; 20.6.9.1) настаивает на том, что распределитель по умолчанию имеет функции-члены:

pointer address(reference x) const noexcept;
const_pointer address(const_reference x) const noexcept;

но если вы создаете экземпляр с аргументом шаблона const (который, по-видимому, UB), то reference и const_reference являются одним и тем же типом, поэтому объявления дублируются. (Тело определения идентично, для чего оно стоит.) Следовательно, никакой контейнер с поддержкой allocator не может иметь явно тип значения const. Скрытие const внутри a tuple позволяет компоновщику создавать экземпляр. Требование этого распределителя от стандарта использовалось для оправдания закрытия хотя бы нескольких старых ошибок libstdС++ о проблемах с std::vector<const int>, хотя это не кажется мне солидной принципиальной точкой. Кроме того, libС++ работает с проблемой очевидным простым способом, который заключается в предоставлении специализации allocator<const T> с удалением дублированных деклараций функций.

4b9b3361

Ответ 1

В С++ 03 Assignable была определена в таблице 64 в §23.1/4,

    Expression    Return type    Post-condition
    t = u         T&             t is equivalent to u

С одной стороны, это требование не выполнялось для std::map. С другой стороны, это было слишком строгое требование для std::list. И С++ 11 продемонстрировал, что он вообще не нужен для std::vector, но наложен использованием определенных операций (например, назначение).

В С++ 11 соответствующее требование называется CopyAssignable и определено в таблице 23 в §17.6.3.1/2,

    Expression    Return type    Return value    Post-condition
    t = v         T&             t               t is equivalent to v, the
                                                 value of v is unchanged

Основные отличия заключаются в том, что элементы контейнера больше не должны быть CopyAssignable и что существует соответствующее требование MoveAssignable.

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

Единственное требование к типу элемента, независимого от операции, в С++ 11, насколько я могу видеть (из таблицы 96 в п. 23.2.1/4), что оно должно быть Destructible.


Что касается std::is_assignable, он не совсем проверяет критерий CopyAssignable.

Здесь следует, согласно таблице 49 в С++ 11 §20.9.4.3/3:

"Выражение declval<T>() = declval<U>() является хорошо сформированный при обработке как неоцененный операнд (Пункт 5). доступ проверка выполняется так, как если бы в контексте, не связанном с Tи U. Только действительность непосредственный контекст выражение присваивания Считается. [Обратите внимание компиляция выражение может привести к побочные эффекты, такие как создание класса специализированные шаблоны и шаблон функции специализации, генерации неявно определяется функций и т.д. такие побочные эффекты отсутствуют в" непосредственный контекст "и может привести к будучи плохо сформированным. -конец примечание]"

В сущности, это подразумевает проверку совместимости типа доступа/существования + аргумента типа operator= и ничего более.

Однако Visual С++ 11.0, похоже, не выполняет проверку доступа, а g++ 4.7.1 дросселирует на нем:

#include <iostream>
#include <type_traits>
#include <tuple>
using namespace std;

struct A {};
struct B { private: B& operator=( B const& ); };

template< class Type >
bool isAssignable() { return is_assignable< Type, Type >::value;  }

int main()
{
    wcout << boolalpha;
    wcout << isAssignable< A >() << endl;              // OK.
    wcout << isAssignable< B >() << endl;              // Uh oh.
}

Создание с Visual С++ 11.0:

[D:\dev\test\so\assignable]
> cl assignable.cpp
assignable.cpp

[D:\dev\test\so\assignable]
> _

Создание с g++ 4.7.1:

[D:\dev\test\so\assignable]
> g++ assignable.cpp
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In substitution of 'template static decltype (((declval)()=(declval)(), std::__sfinae_types::__one()))std::__is_assignable_helper::__test(int) [with _
Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [with _Tp1 = B; _Up1 = B]':
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68:   required from 'constexpr const bool std::__is_assignable_helper::value'
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12:   required from 'struct std::is_assignable'
assignable.cpp:10:59:   required from 'bool isAssignable() [with Type = B]'
assignable.cpp:16:32:   required from here
assignable.cpp:7:24: error: 'B& B::operator=(const B&)' is private
In file included from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40,
                 from assignable.cpp:1:
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049:2: error: within this context
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In substitution of 'template static decltype (((declval)()=(declval)(), std::__sfinae_types::__one())) std::__is_assignable_helper::__test(int) [with _
Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [with _Tp1 = B; _Up1 = B]':
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68:   required from 'constexpr const bool std::__is_assignable_helper::value'
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12:   required from 'struct std::is_assignable'
assignable.cpp:10:59:   required from 'bool isAssignable() [with Type = B]'
assignable.cpp:16:32:   required from here
assignable.cpp:7:24: error: 'B& B::operator=(const B&)' is private
In file included from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40,
                 from assignable.cpp:1:
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049:2: error: within this context
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits: In instantiation of 'constexpr const bool std::__is_assignable_helper::value':
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1060:12:   required from 'struct std::is_assignable'
assignable.cpp:10:59:   required from 'bool isAssignable() [with Type = B]'
assignable.cpp:16:32:   required from here
assignable.cpp:7:24: error: 'B& B::operator=(const B&)' is private
In file included from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/move.h:57:0,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair.h:61,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase.h:65,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits.h:41,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40,
                 from d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40,
                 from assignable.cpp:1:
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055:68: error: within this context

[D:\dev\test\so\assignable]
> _

Итак, суммируя, стандартный std::is_assignable, по-видимому, имеет очень ограниченную утилиту, и на момент написания этой статьи на переносимый код нельзя полагаться.


ИЗМЕНИТЬ: Замените <utility> правильной <type_traits. Интересно, что это не имело значения для g++. Даже для сообщения об ошибке, поэтому я просто позволяю этому быть таким, каким он был.

Ответ 2

Я даю это Alf, но я хотел добавить пару заметок для справок в будущем.

Как говорит Альф, std::is_*_assignable действительно проверяет наличие (явный или неявный) существования соответствующего оператора присваивания. Они не обязательно проверяют, будет ли он корректно сформирован при создании экземпляра. Это отлично подходит для операторов присваивания по умолчанию. Если объявлен член const, операторы присваивания по умолчанию будут удалены. Если базовый класс имеет удаленный оператор присваивания, оператор присваивания по умолчанию будет удален. Поэтому, если вы просто позволяете дефолтам делать свое дело, все должно быть хорошо.

Однако, если вы объявите operator=, это станет вашей ответственностью (если вам интересно), чтобы убедиться, что он был удален. Например, это будет компилироваться и выполняться (по крайней мере, с clang) и сообщает, что C is_assignable:

#include <iostream>
#include <type_traits>
#include <tuple>
using namespace std;

struct A { const int x; A() : x() {}};

struct C { 
  struct A a;
  C& operator=( C const& other);
};

template< class Type >
bool isAssignable() { return is_assignable< Type&, const Type& >::value;  }

int main()
{
    wcout << boolalpha;
    wcout << isAssignable< A >() << endl; // false
    wcout << isAssignable< C >() << endl; // true
    C c1;
    C c2;
}

Отсутствие определения оператора присваивания не указывается до момента ссылки, и в этом случае вовсе не потому, что оператор присваивания никогда не используется. Но учтите, что использование C::operator=, зависящее от std::is_assignable, разрешено компилировать. Конечно, я не мог бы определить C::operator= таким образом, чтобы привести его к члену a, потому что этот член не назначен.

Это не особо интересный пример. Интересным является использование шаблонов, таких как проблема std::tuple, которая запустила весь этот вопрос. Добавьте в шаблон несколько шаблонов и на самом деле определите C::operator= через назначение его члену a:

using namespace std;

template<bool> struct A {
  A() : x() {}
  const int x;
};

template<bool B> struct C {
  struct A<B> a;
  C& operator=( C const& other) {
    this->a = other.a;
    return *this;
  }
};  

template< class Type >
bool isAssignable() { return is_assignable< Type&, const Type& >::value;  }

int main()
{   
    wcout << boolalpha;
    wcout << isAssignable< A<false> >() << endl; // false
    wcout << isAssignable< C<false> >() << endl; // true
    C<false> c1;
    C<false> c2;
    c1 = c2;                                     // Bang
    return 0;
}   

Без назначения в конце код компилируется и запускается (под заголовком 3.3) и сообщает, что A<false> не назначается (правильно), но C<false> можно присваивать (сюрприз!). Фактическая попытка использовать C::operator= показывает правду, потому что только до этого момента компилятор пытается фактически создать экземпляр этого оператора. До этого момента и через экземпляры is_assignable оператор был просто объявлением открытого интерфейса, который, как говорит Альф, - все, что действительно ищет std::is_assignable.

Уф.

Итак, я думаю, что это недостаток как стандартных, так и стандартных реализаций библиотек в отношении стандартных агрегатных объектов, чей operator= должен быть удален, если какой-либо из типов компонентов не может быть назначен. Для std::tuple, & sect; 20.4.2.2 перечисляет в качестве требований для operator=, что все типы компонентов могут быть назначены, и существуют аналогичные требования для других типов, но я не думаю, что это требование требует, чтобы разработчики библиотек удалили неприменимый operator=.

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