根据 C++,该程序在哪个确切的语句中表现出未定义的行为?标准?

发布于 2024-12-26 05:20:51 字数 797 浏览 5 评论 0原文

(我知道应该避免返回对函数局部变量的地址/引用,并且程序永远不应该这样做。)


返回对局部变量/引用的引用是否会导致未定义的行为?或者,未定义的行为是否仅在稍后使用返回的引用(或“取消引用”)时发生?

即,下面的代码示例在什么确切的语句(#1#2#3)处调用未定义的行为? (我已经将我的理论与每个理论一起写下来)

#include <iostream>

struct A
{ 
   int m_i;
   A():m_i(10)
   {

   } 
};  
A& foo() 
{     
    A a;
    a.m_i = 20;     
    return a; 
} 

int main()
{
   foo();                // #1 - Not UB; return value was never used
   A const &ref = foo(); // #2 - Not UB; return value still not yet used
   std::cout<<ref.m_i;   // #3 - UB: returned value is used
}

我有兴趣知道 C++ 标准在这方面具体规定了什么。

我想要 C++ 标准的引用,它基本上可以告诉我哪个确切的语句使该代码格式不正确。

欢迎讨论具体实现如何处理这个问题,但正如我所说,理想的答案将引用 C++ 标准中的参考文献,毫无疑问地澄清了这一点。

(I am aware of the fact that returning address/reference to a variable local to the function should be avoided and a program should never do this.)


Does returning a reference to a local variable/reference result in Undefined Behavior? Or does the Undefined Behavior only occur later, when the returned reference is used (or "dereferenced")?

i.e. at what exact statement (#1 or #2 or #3) does code sample below invoke Undefined Behavior? (I've written my theory alongside each one)

#include <iostream>

struct A
{ 
   int m_i;
   A():m_i(10)
   {

   } 
};  
A& foo() 
{     
    A a;
    a.m_i = 20;     
    return a; 
} 

int main()
{
   foo();                // #1 - Not UB; return value was never used
   A const &ref = foo(); // #2 - Not UB; return value still not yet used
   std::cout<<ref.m_i;   // #3 - UB: returned value is used
}

I am interested to know what the C++ standard specifies in this regard.

I would like a citation from the C++ standard which will basically tell me which exact statement makes this code ill-formed.

Discussions about how specific implementations handle this are welcome but as I said an ideal answer would cite an reference from the C++ Standard that clarifies this beyond doubt.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

箜明 2025-01-02 05:20:51

当然,当引用首次初始化时,它是有效完成的,满足以下条件:

[C++11: 8.3.2/5]: 不得有对引用的引用、不得有引用数组、不得有指向引用的指针。引用的声明应包含初始值设定项 (8.5.3),除非声明包含显式 extern 说明符 (7.1.1),是类定义中的类成员 (9.2) 声明,或者是参数或参数的声明返回类型(8.3.5);见 3.1。 应初始化引用以引用有效的对象或函数。 [ 注意: 特别是,空引用不能存在于明确定义的程序中,因为唯一的方法创建这样的引用会将其绑定到通过取消引用空指针获得的“对象”,这会导致未定义的行为。如 9.6 中所述,引用不能直接绑定到位字段。 —尾注]

从函数返回的引用是一个xvalue

[C++11: 3.10/1]: [..] xvalue(“eXpiring”值)也指对象,通常接近其生命周期结束时(因此例如,其资源可能会被移动)。 xvalue 是涉及右值引用的某些类型表达式的结果 (8.3.2)。 [示例:调用返回类型为右值引用的函数的结果是 xvalue。 —结束示例 ] [..]

这意味着以下内容适用:

[C++11: 12.2/1]: 类类型的临时对象是在各种上下文中创建的:将引用绑定到纯右值 (8.5.3)、返回纯右值 ( 6.6.3),创建纯右值的转换 (4.1, 5.2.9, 5.2.11, 5.4),抛出异常(15.1),进入处理程序(15.3),以及一些初始化(8.5)。

[C++11: 6.6.3/2]: 既没有表达式也没有花括号初始化列表的返回语句> 只能用在不返回值的函数中,即返回类型为 void 的函数、构造函数(12.1)或析构函数(12.4)。

带有非void类型表达式的return语句只能在返回值的函数中使用;表达式的值返回给函数的调用者。 表达式的值会隐式转换为其所在函数的返回类型。 return 语句可以涉及临时对象的构造和复制或移动 (12.2)。 [ 注意: 与 return 语句关联的复制或移动操作可以被省略或被视为右值用于选择构造函数时进行重载决策 (12.8)。 —尾注 ] 带有braced-init-list的return语句通过copy-list-initialization初始化要从函数返回的对象或引用(8.5.4)来自指定的初始值设定项列表。 [示例:

std::pair; f(const char* p, int x) {
   返回 {p,x};
}

—结束示例]

此外,即使我们将以下内容解释为执行新引用“对象”的初始化,裁判也是当时可能还活着:

[C++11: 8.5.3/2]: 初始化后无法将引用更改为引用另一个对象。请注意,引用的初始化与对其赋值的处理方式非常不同。 参数传递(5.2.2)和函数值返回(6.6.3)是初始化。

  • 这使得 #1 有效。

但是,您对新引用的初始化 main 内的 ref 明显违反了 [C++11: 8.3.2/5]。我找不到它的措辞,但按理说,执行初始化时函数作用域已退出。

  • 这将使#2(以及因此#3)无效。

至少,标准中似乎没有关于此事的任何进一步说明,因此如果上述推理不充分,那么我们不得不得出结论,该标准在这件事上是含糊不清的。幸运的是,它在实践中影响不大,至少在主流中是这样。

Of course, when the reference is first initialised it is done so validly, satisfying the following:

[C++11: 8.3.2/5]: There shall be no references to references, no arrays of references, and no pointers to references. The declaration of a reference shall contain an initializer (8.5.3) except when the declaration contains an explicit extern specifier (7.1.1), is a class member (9.2) declaration within a class definition, or is the declaration of a parameter or a return type (8.3.5); see 3.1. A reference shall be initialized to refer to a valid object or function. [ Note: in particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by dereferencing a null pointer, which causes undefined behavior. As described in 9.6, a reference cannot be bound directly to a bit-field. —end note ]

The reference being returned from the function is an xvalue:

[C++11: 3.10/1]: [..] An xvalue (an “eXpiring” value) also refers to an object, usually near the end of its lifetime (so that its resources may be moved, for example). An xvalue is the result of certain kinds of expressions involving rvalue references (8.3.2). [ Example: The result of calling a function whose return type is an rvalue reference is an xvalue. —end example ] [..]

That means the following does not apply:

[C++11: 12.2/1]: Temporaries of class type are created in various contexts: binding a reference to a prvalue (8.5.3), returning a prvalue (6.6.3), a conversion that creates a prvalue (4.1, 5.2.9, 5.2.11, 5.4), throwing an exception (15.1), entering a handler (15.3), and in some initializations (8.5).

[C++11: 6.6.3/2]: A return statement with neither an expression nor a braced-init-list can be used only in functions that do not return a value, that is, a function with the return type void, a constructor (12.1), or a destructor (12.4).

A return statement with an expression of non-void type can be used only in functions returning a value; the value of the expression is returned to the caller of the function. The value of the expression is implicitly converted to the return type of the function in which it appears. A return statement can involve the construction and copy or move of a temporary object (12.2). [ Note: A copy or move operation associated with a return statement may be elided or considered as an rvalue for the purpose of overload resolution in selecting a constructor (12.8). —end note ] A return statement with a braced-init-list initializes the object or reference to be returned from the function by copy-list-initialization (8.5.4) from the specified initializer list. [ Example:

std::pair<std::string,int> f(const char* p, int x) {
   return {p,x};
}

—end example ]

Additionally, even if we interpret the following to mean that an initialisation of a new reference "object" is performed, the referee is probably still alive at the time:

[C++11: 8.5.3/2]: A reference cannot be changed to refer to another object after initialization. Note that initialization of a reference is treated very differently from assignment to it. Argument passing (5.2.2) and function value return (6.6.3) are initializations.

  • This makes #1 valid.

However, your initialisation of a new reference ref inside main quite clearly violates [C++11: 8.3.2/5]. I can't find wording for it, but it stands to reason that the function scope has been exited when the initialisation is performed.

  • This would make #2 (and consequently #3) invalid.

At the very least, there does not appear to be anything further stated about the matter in the standard, so if the above reasoning is not sufficient then we have to conclude that the standard is ambiguous in the matter. Fortunately, it's of little consequence in practice, at least in the mainstream.

如果没结果 2025-01-02 05:20:51

这是我对此事的不完整且可能不充分的看法:

引用的唯一特殊之处是在初始化时它们必须引用一个有效的对象。如果该对象稍后不再存在,则使用该引用是 UB,因此初始化另一个对现已失效的引用的引用也是如此。

下面更简单的示例提供了与您的问题完全相同的困境,我认为:

std::reference_wrapper<T> r;

{
    T t;
    r = std::ref(t);
}

// #1

在#1处,r内的引用不再有效,但程序很好。只是不要阅读r

在您的示例中,行 #1 没问题,而行 #2 则不行——这是因为原始行 #2 使用参数 调用 A::A(A const &) foo(),正如所讨论的,这无法使用有效引用初始化函数参数变量,您编辑的版本A const & 也是如此。 a = foo();

Here's my incomplete and possible insufficient view on the matter:

The only thing special about references is that at initialization time they must refer to a valid object. If the object later stops existing, using the reference is UB, and so is initializing another reference to the now-defunct reference.

The following much simpler example provides exactly the same dilemma as your question, I think:

std::reference_wrapper<T> r;

{
    T t;
    r = std::ref(t);
}

// #1

At #1, the reference inside r is no longer valid, but the program is fine. Just don't read r.

In your example, line #1 is fine, and line #2 isn't -- that is because the original line #2 calls A::A(A const &) with argument foo(), and as discussed, this fails to initialize the function argument variable with a valid reference, and so would your edited version A const & a = foo();.

子栖 2025-01-02 05:20:51

我会说#3。即使引用的对象已经超出范围,#2 本身也不会实际执行任何操作。这实际上并不是一个与标准相关的问题,因为它是连续犯下两个错误的结果:

  1. 返回对超出范围的对象的引用,然后
  2. 使用引用。

要么孤立地具有定义的行为。该标准是否对在生命周期结束后使用对象的引用有任何规定是另一回事。

I would say #3. Alone, #2 doesn't actually do anything even though the referenced object is already out of scope. This isn't really a standards-related issue because it is the result of two mistakes made in succession:

  1. Returning a reference to an out-of-scope object followed by
  2. Use of a reference.

Either in isolation has defined behavior. Whether the standard has anything to say regarding use of references to objects beyond the end of their lifetime is another matter.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文