【问题标题】:std::launder and strict aliasing rulestd::launder 和严格的别名规则
【发布时间】:2018-12-14 16:57:34
【问题描述】:

考虑这段代码:

void f(char * ptr)
{
    auto int_ptr = reinterpret_cast<int*>(ptr); // <---- line of interest 
    // use int_ptr ... 
}

void example_1()
{
    int i = 10;    
    f(reinterpret_cast<char*>(&i));
}

void example_2()
{
    alignas(alignof(int)) char storage[sizeof(int)];
    new (&storage) int;
    f(storage);
}

来自example_1的电话感兴趣的线路:

Q1:在调用端,char 指针是我们整数指针的别名。这是有效的。但是将其转换回int 也有效吗?我们知道int 在其生命周期内,但考虑到该函数在另一个翻译单元中定义(未启用链接时间优化)并且上下文未知。那么编译器看到的就是:一个int 指针想要给一个char 指针起别名,这违反了严格的别名规则。那么允许吗?

Q2:考虑到这是不允许的。我们在 C++17 中得到了std::launder。这是一种指针优化屏障,主要用于访问将new'ed 放置到其他类型对象的存储中或涉及const 成员时的对象。我们可以用它来给编译器一个提示并防止未定义的行为吗?

来自 example_2 的调用感兴趣的行:

Q3:这里应该需要std::launder,因为这是Q2中描述的std::launder用例,对吧?

auto int_ptr = std::launder(reinterpret_cast<int*>(ptr));

但再次考虑f 是在另一个翻译单元中定义的。编译器如何知道我们的位置new,它发生在调用端?编译器(只看到函数f)如何区分example_1example_2?或者以上只是假设,因为严格的别名规则只会排除一切(记住,char*int* 不允许)并且编译器可以做它想做的事?

后续问题:

Q4:如果上面的所有代码都因为别名规则而出错,考虑将函数f改成一个空指针:

void f(void* ptr)
{
    auto int_ptr = reinterpret_cast<int*>(ptr); 
    // use int_ptr ... 
}

那么我们就没有别名问题了,但是example_2 仍然存在std::launder 的情况。我们是否已更改调用方并将我们的example_2 函数重写为:

void example_2()
{
    alignas(alignof(int)) char storage[sizeof(int)];
    new (&storage) int;
    f(std::launder(storage));
}

或者函数f中的std::launder足够了吗?

【问题讨论】:

    标签: c++ language-lawyer c++17 strict-aliasing placement-new


    【解决方案1】:

    严格的别名规则是对实际用于访问对象的泛左值类型的限制。就该规则而言,重要的是 a) 对象的实际类型,以及 b) 用于访问的 glvalue 的类型。

    指针经过的中间转换是无关紧要的,只要它们保留指针值。 (这是双向的;再多的聪明的演员阵容 - 或洗钱,就此而言 - 都无法解决严格的混叠违规。)

    只要ptr 实际指向int 类型的对象,f 就有效,假设它通过int_ptr 访问该对象而无需进一步转换。

    example_1 书面有效; reinterpret_casts 不会改变指针值。

    example_2 是无效的,因为它为f 提供了一个实际上并不指向int 对象的指针(它指向storage 数组的过期第一个元素)。见Is there a (semantic) difference between the return value of placement new and the casted value of its operand?

    【讨论】:

    • 哇,为什么 example_2 错误的解释 - 连同链接 - 很有启发性。泰!检查我的理解:如果我将在example_2 中对 f 的调用重写为:f(reinterpret_cast&lt;char*&gt;(std::launder(reinterpret_cast&lt;int*&gt;(&amp;storage)))); 这将是有效的,对吗?步骤是: 1. 获取一个 int*,它仍然指向存储的第一个元素。 2. 然后清洗它以获得“有效”的 int*。 3. 然后将其转换为 char*,因为该函数需要 char*。对吗?
    • @phön @T.C.我想知道我们是否可以通过auto int_ptr = std::launder( reinterpret_cast&lt;int*&gt;(ptr) ); 修复f(),就像在const int h = std::launder(reinterpret_cast&lt;Y*&gt;(&amp;s))-&gt;z; // OKcppreference 一样。我们是否允许在不取消引用的情况下传递指向过期对象的指针?
    • @phön 啊,我想我找到了basic.compound/3.4 无效指针。
    • 只有当底层存储消失时,指针才会失效。简单地超出生命周期(由于存储重用)就可以了。在f 进行洗钱是有效的,但似乎值得怀疑,因为这意味着即使是不需要它的呼叫者,您也要支付洗钱费用(如果有的话)。
    • @T.C. only invalid when the underlying storage is gone:请参考标准条款。在我看来,根据basic.life/8“可以通过调用std​::​launder从一个表示其存储地址的指针中获得指向新对象的指针”。我猜这个 note 暗示旧指针已经变成 invalid 因为该指针在没有std::launder 的情况下诱导UB。
    【解决方案2】:

    你不需要在函数 f() 中使用 std::launder。尽可能通用,函数 f() 可以与任何指针一起使用:脏(需要清洗)或不脏。它只在调用端知道,所以你必须使用这样的东西:

    void example_2()
    {
        alignas(alignof(int)) char storage[sizeof(int)];
        new (&storage) int;        // char[] has gone, now int there
        f(std::launder(reinterpret_cast<int*>(storage)));  // assiming f(void* p)
    }
    

    而 f() 本身就是另一回事了。正如您所提到的,考虑 f() 被放置在共享库中,因此根本没有任何上下文假设。

    【讨论】:

      猜你喜欢
      • 2023-02-02
      • 1970-01-01
      • 2015-10-15
      • 2017-02-25
      • 1970-01-01
      • 2018-05-23
      • 2014-07-13
      • 2015-03-30
      相关资源
      最近更新 更多