“可分配”究竟意味着什么?

时间:2012-12-23 06:25:48

标签: c++ c++11 language-lawyer

C ++ 11删除了所有容器的值类型为CopyConstructible和Assignable的要求(尽管容器上的特定操作可能会强加这些要求)。从理论上讲,这应该可以定义,例如,std::deque<const Foo>,这在C ++ 03中是不可能的。

出乎意料的是,当我尝试这个时,gcc 4.7.2产生了通常的难以理解的错误[1],但是clang至少使错误可读并且与libc ++一起编译它没有错误。

现在,当两个不同的编译器产生不同的结果时,它总是让我想知道正确的答案是什么,所以我搜索了所有可以找到const / assignable / value types / containers等的引用。我发现了差不多十年的非常相似的问题和答案,其中一些在SO和其他C ++邮件列表中,其他地方,包括Gnu buganizer,所有这些基本上可以概括为以下对话。

问:为什么我不能宣布std::vector<const int>(作为简化示例)

答:你为什么要这样做?这是荒谬的。

问:嗯,这对我来说很有道理。为什么我不能这样做?

答:因为标准要求值类型可分配。

问:但我不打算分配它们。我希望它们在我创建它们之后成为const。

答:这不是它的工作方式。下一个问题!

带着温和的潇洒:

A2:C ++ 11决定允许这样做。你只需要等待。与此同时,重新思考你的荒谬设计。

这些似乎不是非常引人注目的答案,尽管我可能有偏见,因为我属于“但这对我有意义”的范畴。在我的情况下,我想有一个类似堆栈的容器,其中被推入堆栈的东西是不可变的,直到它们被弹出,这不会让我感到特别奇怪的是希望能够用类型表达系统

无论如何,我开始考虑答案,“标准要求所有容器的值类型都可以分配。”而且,据我所知,现在我发现了C ++ 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和gcc)。当然,它不是真正可转让的;尝试编译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 Foostd::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>&]’

所以这里发生的是标准(第20.6.9.1节)坚持默认分配器具有成员函数:

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

但是如果使用const模板参数(显然是UB)实例化它,那么referenceconst_reference是相同的类型,因此声明是重复的。 (定义的主体是相同的,因为它的价值。)因此,没有分配器感知的容器可以处理显式const值类型。将const隐藏在tuple内允许分配器实例化。标准中的这个分配器要求被用来证明关于std::vector<const int>的问题至少关闭了几个旧的libstdc ++错误,尽管它并没有把我作为一个坚实的原则。此外,libc ++以明显简单的方式解决问题,即提供allocator<const T>的特化,删除重复的函数声明。

2 个答案:

答案 0 :(得分:12)

在C ++ 03中,Assignable由§23.1/ 4中的表64定义,

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

一方面,std::map未满足此要求。另一方面,对std::list的要求过于严格。而C ++ 11表明,std::vector通常不需要它,但是通过使用某些操作(例如赋值)来强加它。

在C ++ 11中,相应的要求名为CopyAssignable,由§17.6.3.1/ 2中的表23定义,

    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数据成员的结构显然不可转让,除非选择用非常特殊的解释来阅读“等同于”。

据我所知,C ++ 11中唯一与操作无关的元素类型要求(来自§23.2.1/ 4中的表96)必须是Destructible


关于std::is_assignable,它并未完全测试CopyAssignable标准。

根据C ++11§20.9.4.3/ 3中的表49,这是std::is_assignable<T, U>所暗示的内容:

  

“表达   declval<T>() = declval<U>()是   处理后形成良好   作为一个未经评估的操作数   (第5条)。访问   检查就像执行一样   在与T无关的上下文中   和U。只有有效期   直接的背景   赋值表达式   被认为。 [注意:   汇编   表达式可以导致   副作用如   类的实例化   模板专业化   和功能模板   专业化,   一代人   隐式定义   功能等。这样   副作用不在   “直接背景”和   可以导致该计划   形象不对。 末端   注意]“

本质上,这意味着operator=的访问/存在+参数类型兼容性检查,仅此而已。

但是,Visual C ++ 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 C ++ 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 ++无关。甚至不是错误消息,所以我只是让它保持原样。

答案 1 :(得分:4)

我将此问题发给Alf,但我想添加一些注释以供将来参考。

正如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;
}   

在最后没有赋值,代码编译并运行(在clang 3.3下)并报告A<false>不可分配(正确)但C<false>可分配(惊讶!)。使用C::operator=的实际尝试揭示了真相,因为直到那一点,编译器才试图实际实例化该运算符。到那时为止,通过is_assignable的实例化,运算符只是一个公共接口的声明,正如Alf所说的那样 - std::is_assignable正在寻找的所有内容。

呼。

所以底线,我认为对于标准聚合对象而言,这是标准库和标准库实现中的缺陷,如果任何组件类型不可分配,则应删除operator=。对于std::tuple,§20.4.2.2列出了operator=的所有组件类型可分配的要求,并且对其他类型有类似的要求,但我不认为该要求要求库实现者删除不适用operator=

但是,据我所知,没有什么能阻止库实现删除(除了有条件地删除赋值运算符的烦恼因素)。在我看来这几天后,他们应该这样做,而且标准应该要求他们这样做。否则,可靠地使用is_assignable就不可能了。