我一直试图理解 std::nullopt_t
的基本原理不允许是DefaultConstructible
在 C++17(它被引入的地方)及更高版本中,并在此过程中克服了一些编译器差异混淆。
考虑以下违反规范的(它是 DefaultConstructible
)实现 nullopt_t
:
struct nullopt_t {
explicit constexpr nullopt_t() = default;
};
这是 C++11 和 C++14(无用户提供的 ctor)中的聚合,但不是 C++17(explicit
ctor)和 C++20(用户声明的 ctor)中的聚合.现在考虑以下示例:
struct S {
constexpr S() {}
S(S const&) {}
S& operator=(S const&) { return *this; } // #1
S& operator=(nullopt_t) { return *this; } // #2
};
int main() {
S s{};
s = {}; // GCC error: ambiguous overload for 'operator=' (#1 and #2)
}
这在 C++11 到 C++20 中被 GCC(各种版本,比如 v11.0)拒绝,但在 C++11 到 C++ 中被 Clang(比如 v12.0)和 MSVC(v19.28)接受C++20。DEMO
我最初的假设是该程序:
nullopt_t
(如上)是一个聚合,而它 nullopt_t
的复制列表初始化#2
处的复制赋值运算符所需的对象可行,但是没有一个编译器完全同意这个理论,有些我可能遗漏了一些东西。
什么编译器在这里是正确的(如果有的话),我们如何通过相关的标准部分(和 DR:s,如果相关)来解释它?
最佳答案
为什么是 nullopt_t
必须是 DefaultConstructible
首先?nullopt_t
的规范要求不得为 DefaultConstructible
回想起来,可以说是一个基于标签类型的一些 LWG 和 CWG 混淆的错误,并且这种混淆的解决仅在 std::optional
之后才出现。是 brought in from the Library Fundamentals TS Components .
首先,nullopt_t
的当前 (C++17, C++20) 规范, [optional.nullopt]/2 , 需要 [ 重点矿]:
Type
nullopt_t
shall not have a default constructor or an initializer-list constructor, and shall not be an aggregate.
其主要用途在上一节中描述,[optional.nullopt]/1 :
[...] In particular,
optional<T>
has a constructor withnullopt_t
as a single argument; this indicates that an optional object not containing a value shall be constructed.
现在,P0032R3 (
variant
、any
和 optional
的同构界面),其中一篇论文是介绍 std::optional
的一部分, 有一个关于 nullopt_t
的讨论, 一般的标签类型和 DefaultConstructible
需求 [ 重点矿]:No default constructible
While adapting
optional<T>
to the newin_place_t
type we found that we cannot anymore usein_place_t{}
. The authors don't consider this a big limitation as the user can usein_place
instead. It needs to be noted that this is in line with the behavior ofnullopt_t
asnullopt_t{}
fails as no default constructible. Howevernullptr_t{}
seems to be well formed.Not assignable from
{}
After a deeper analysis we found also that the old
in_place_t
supportedin_place_t t = {};
. The authors don't consider this a big limitation as we don't expect that a lot of users could use this and the user can usein_place
instead.in_place_t t; t = in_place;
It needs to be noted that this is in line with the behavior of
nullopt_t
as the following compile fails.nullopt_t t = {}; // compile fails
However
nullptr_t
seems to be support it.nullptr_t t = {}; // compile pass
To re-enforce this design, there is an pending issue 2510-Tag types should not be
DefaultConstructible
Core issue 2510.
事实上,最初提议的 LWG Core Issue 2510 的决议是要求所有标签类型都不是
DefaultConstructible
[ 重点矿]:(LWG) 2510. Tag types should not be
DefaultConstructible
[...]
Previous resolution [SUPERSEDED]:
[...] Add a new paragraph after 20.2 [utility]/2 (following the header synopsis):
- -?- Type
piecewise_construct_t
shall not have a default constructor. It shall be a literal type. Constantpiecewise_construct
shall be initialized with an argument of literal type.
然而,该决议已被取代,因为与 CWG Core Issue 1518 有重叠。 ,最终以不需要标签类型不为
DefaultConstructible
的方式解决。 ,如 explicit
就足够了[ 重点矿]:(CWG) 1518. Explicit default constructors and copy-list-initialization
[...]
Additional note, October, 2015:
It has been suggested that the resolution of issue 1630 went too far in allowing use of explicit constructors for default initialization, and that default initialization should be considered to model copy initialization instead. The resolution of this issue would provide an opportunity to adjust that.
Proposed resolution (October, 2015):
Change 12.2.2.4 [over.match.ctor] paragraph 1 as follows:
[...] For direct-initialization
or default-initialization, the candidate functions are all the constructors of the class of the object being initialized. [...]
只要
explicit
还暗示该类型不是聚合,这反过来是 LWG Core Issue 2510 的最终解决方案(基于 CWG Core Issue 1518 的最终解决方案)(LWG) 2510. Tag types should not be
DefaultConstructible
[...]
Proposed resolution:
[...] In 20.2 [utility]/2, change the header synopsis:
// 20.3.5, pair piecewise construction struct piecewise_construct_t { explicit piecewise_construct_t() = default; }; constexpr piecewise_construct_t piecewise_construct{};
[...]
然而,后面的这些更改并未包含在
std::optional
的提案中。 ,可以说是疏忽,我想声明nullopt_t
不必是DefaultConstructible
, 只是,与其他标签类型一样,它应该有一个用户声明的 explicit
构造函数,它禁止它作为空括号复制列表初始化的候选对象,因为它不是一个聚合,并且唯一的候选构造函数是 explicit
.哪个编译器在这里是对还是错?
鉴于 LWG 2510、CWG 1518(和其他)混淆,让我们关注 C++17 及更高版本。在这种情况下,GCC 拒绝该程序可以说是错误的,而 Clang 和 MSVC 接受它是正确的。
为什么?
因为
S& operator=(nullopt_t)
赋值运算符不适用于赋值 s = {};
, 作为空括号 {}
将需要聚合初始化或复制列表初始化来创建 nullopt_t
(临时)对象。 nullopt_t
,但是(通过惯用的标签实现:我上面的实现),根据 P0398R0 (解决了 CWG 核心问题 1518),既不是聚合,也不是其默认构造函数参与复制列表初始化(来自空括号)。这可能属于以下 GCC 错误报告:
被列为
SUSPENDED
2015 年 6 月 15 日,在 CWG 核心问题 1630 的决议更改之前(“问题 1630 的决议走得太远了”)。现在根据来自此问答的 ping 重新打开票证。
关于c++ - 来自空括号的不明确复制分配的编译器差异,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67469039/