27

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>(作为一个简化的例子)

A:你到底为什么要这么做?这是荒谬的。

问:嗯,这对我来说很有意义。为什么我做不到?

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

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

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

伴随着温和的冲刺:

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

这些似乎不是很有说服力的答案,尽管我可能有偏见,因为我属于“但对我来说有意义”的类别。就我而言,我希望有一个类似堆栈的容器,其中推入堆栈的东西在弹出之前是不可变的,这对我来说并不是一件特别奇怪的事情,希望能够用一种类型来表达系统。

无论如何,我开始思考答案,“标准要求所有容器的值类型都是可分配的。” 而且,据我所知,现在我找到了 C++03 标准草案的旧副本,这是真的;它做了。

另一方面, is 的值类型在std::map我看来不像是可分配的。尽管如此,我还是再次尝试了,并且 gcc 继续编译它而没有眨眼。所以至少我有某种解决方法。std::pair<const Key, T>std::deque<std::tuple<const Foo>>

然后我尝试打印出std::is_assignable<const Foo, const Foo>and的值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值类型的容器,或者他们是否有其他不可分配的情况?

const Foo2)和的常数之间真的存在显着差异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>&]’

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

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

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

4

2 回答 2

12

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

    表达式 返回类型 后置条件
    t = u T& t 等价于 u

一方面,这个要求没有得到满足std::map。另一方面,它的要求太严格了std::list。并且 C++11 证明了std::vector,一般来说,它甚至不是必需的,而是通过使用某些操作(例如赋值)来强加的。

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

    表达式 返回类型 返回值 后置条件
    t = v T& tt 等价于 v,即
                                                 v 的值不变

主要区别在于不再需要容器元素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 可分配.cpp
可分配的.cpp

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

使用 g++ 4.7.1 构建:

[D:\dev\test\so\assignable]
> g++ 可分配的.cpp
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:替代'模板静态 decltype (((declval)()=(declval)(), std::__sfinae_types::__one()))std::__is_assignable_helper::__test(int) [with _
Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [与_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:'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:从“struct std::is_assignable”中需要
assignable.cpp:10:59:'bool isAssignable() [with Type = B]' 需要
assignable.cpp:16:32:从这里需要
assignable.cpp:7:24:错误:'B& B::operator=(const B&)' 是私有的
在 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ 中包含的文件中位/move.h:57:0,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair .h:61,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase .h:65,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits .h:41,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41 ,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40 ,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40 ,
                 来自assignable.cpp:1:
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049: 2:错误:在这种情况下
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:替代'模板静态 decltype (((declval)()=(declval)(), std::__sfinae_types::__one())) std::__is_assignable_helper::__test(int) [with _
Tp1 = _Tp1; _Up1 = _Up1; _Tp = B; _Up = B] [与_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:'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:从“struct std::is_assignable”中需要
assignable.cpp:10:59:'bool isAssignable() [with Type = B]' 需要
assignable.cpp:16:32:从这里需要
assignable.cpp:7:24:错误:'B& B::operator=(const B&)' 是私有的
在 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ 中包含的文件中位/move.h:57:0,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair .h:61,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase .h:65,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits .h:41,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41 ,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40 ,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40 ,
                 来自assignable.cpp:1:
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1049: 2:错误:在这种情况下
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:在实例化中'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:从“struct std::is_assignable”中需要
assignable.cpp:10:59:'bool isAssignable() [with Type = B]' 需要
assignable.cpp:16:32:从这里需要
assignable.cpp:7:24:错误:'B& B::operator=(const B&)' 是私有的
在 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ 中包含的文件中位/move.h:57:0,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_pair .h:61,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/stl_algobase .h:65,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/bits/char_traits .h:41,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ios:41 ,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/ostream:40 ,
                 来自 d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/iostream:40 ,
                 来自assignable.cpp:1:
d:\bin\mingw\bin\../lib/gcc/i686-pc-mingw32/4.7.1/../../../../include/c++/4.7.1/type_traits:1055: 68:错误:在这种情况下

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

因此,总而言之,该标准的std::is_assignable实用性似乎非常有限,并且在撰写本文时,它不能依赖于可移植代码。


编辑:替换<utility>为正确的<type_traits. 有趣的是,这对 g++ 无关紧要。甚至没有错误消息,所以我就让它保持原样。

于 2012-12-23T08:14:29.787 回答
4

我把这个给了Alf,但我想添加一些注释以供将来参考。

正如 Alf 所说,std::is_*_assignable实际上只检查是否存在(显式或隐式)适当的赋值运算符。他们不一定会检查它是否会在实例化时形成良好的格式。这适用于默认赋值运算符。如果声明了成员const,则默认的赋值运算符将被删除。如果基类有一个已删除的赋值运算符,则默认的赋值运算符将被删除。因此,如果您只是让默认值做他们的事情,那应该没问题。

但是,如果您确实 declare operator=,您有责任(如果您关心的话)确保它被适当地删除。例如,这将编译并运行(至少使用 clang),并报告Cis_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=contingent onstd::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=了所有组件类型都可以分配的要求,并且对其他类型也有类似的要求,但我认为该要求不需要库实现者删除 inapplicable operator=

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

于 2012-12-23T16:43:14.340 回答