在未求值的上下文中(例如需要表达式的情况下),UB是否仍然存在?

18

C++ 20的草案 [concept.default.init]没有精确定义default_initializable

template<class T>
concept default_initializable = constructible_from<T> &&
    requires { T{}; } &&
    is-default-initializable <T>; // exposition-only

针对类型T,当且仅当以下变量定义:

T t;

在某个虚构变量t上是符合格式要求的,is-default-initializable <T> 为真;否则为假。访问检查被执行,就像在与T无关的上下文中一样。只考虑变量初始化的直接上下文的有效性。

cppreference 上,我们找到了一个可能的实现建议:

template<class T>
concept default_initializable =
    std::constructible_from<T> &&
    requires { T{}; } &&
    requires { ::new (static_cast<void*>(nullptr)) T; };

使用空指针参数调用placement-new运算符会导致未定义的行为。
引用自9):单个对象标准placement new表达式调用此函数。标准库实现不执行任何操作并返回未修改的ptr。如果通过placement new表达式调用此函数且ptr是空指针,则其行为未定义。
我的问题是:建议的实现是否有效?一方面,我认为不是,因为涉及到表现出未定义行为的表达式。另一方面,我认为是,因为此表达式出现在未求值上下文中,因此可能不需要具有良好定义的行为(?),只需要在语法上有效即可。但我找不到明确的证据支持其中的任何一种情况。
第二个问题:如果后者被证明是真的,那么为什么这个placement-new构造满足标准对T t;必须是良好形式的要求?对我来说,它看起来像一个奇怪的hack,因为既简单要求也复合要求都没有提供要求T t;的可能性。但是为什么这能行?

2
"如果调用此函数,则未定义"。我认为这不是未定义行为,因为它实际上没有在未评估的上下文中被调用 - HolyBlackCat
4
由于我没有标准引用,所以我只是发表评论,但我确信UB要求实际执行有问题的代码。例如,给定绑定到无效目标(比如,函数已返回的函数局部变量)的int &foo,代码if(false){std::cout<<foo;}不属于UB,因为目标从未被实际使用。换句话说,如果代码没有被执行,就没有“行为”--没有行为,就不可能出现未定义行为。 - cdhowie
1
@cdhowie 我不确定情况是否如此。我想不出一个例子,但是我记得看过一个程序的例子,因为在一个从未被调用的函数中存在 UB,导致程序表现出奇怪的行为。 - cigien
2
@cdhowie 我同意 cigien 的观点。存在未定义行为的代码仅仅就足以让编译器完全改变其余代码的行为,即使有缺陷的代码从未被执行过。请参见 https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633 以获取有趣的分析。 - Mark Ransom
@cdhowie 可能会导致编译器中的 UB,例如通过推动实现的极限。 - Swift - Friday Pie
显示剩余3条评论
1个回答

2
当指定时,未定义的行为是评估 [expr.new]/20 的结果。
如果分配函数是一个返回 null 的非分配形式([new.delete.placement]),则行为是未定义的。

[expr.prim.req]/2:

“出现在需求体中的表达式是未求值的操作数。”

[expr.prop]/1:

“未评估的操作数不会被评估。” 因为放置新分配函数需要返回值,但是此类值没有计算,因此不存在未定义行为。
如果不是这种情况,那么像 decltype( std::declval<int&>() + std::declval <int&> ()) 这样的常见结构也将是未定义行为。

谢谢。您能否详细说明第二个问题:“为什么这种放置新建构造满足标准要求T t;必须是良好形式?”也许有一个更普遍的答案,不会在这个例子上挑剔。换句话说,我可以问:“为什么在评估上下文中将是UB的结构是有效的要求,我们如何检查它们确实需要它们应该需要的东西?”对于像decltype(...)这样的简单事情,我可以看到它(因为某种意义上打字是独立于值的)。但是,是否有正式的论据表明这总是正确的? - UniversE
@UniversE,我认为这很容易从这里的第2点[https://en.cppreference.com/w/cpp/language/default_initialization]中得出结论:“在三种情况下执行默认初始化... 2)通过没有初始化程序的new-expression创建具有动态存储期的对象时...”,仅涵盖动态存储期的情况。 - Secundi
@Secundi 哦,我明白了。但是我认为这违反了标准。因为它说当且仅当对于某个虚构的变量t,“T t;”是良好形式的。但是cppreference实现引入了placement-new,看起来它违反了“当且仅当”的部分。但我必须承认,我还没有检查标准中的最终措辞,那时它是草案,可能已经改变以包括默认可初始化概念的new-expression。我稍后会查看一下。 - UniversE
是的,我也最初遇到了这个问题。归根结底,问题在于:这个表达式(!)的确切严格约束条件是什么?标准中应该有一个通用部分来解释这个问题(自动和动态存储期?)。也许标准中没有明确说明,但对于实际编译器来说,为了使放置 new/动态存储上下文的表达式良好形成,这一点也很重要。 - Secundi
否则,标准库将不得不进一步打破唯一约束,可能会突然转化为非对称的对应物,但是您肯定不希望 default_initializable<T> 在自动存储期和动态存储期情况下有所不同... :) - Secundi
不过我认为这违反了标准。不,因为标准库的实现只是可能的实现之一,通常与自定义约束相关。从编译器层面的角度来看(即表达式层面/语言层面),我同意你的观点。 - Secundi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接