从构造函数中分配包含`std::string`的r值时clang似乎存在错误

7

在测试处理const对象成员时,我遇到了clang中这个明显的bug。该代码在msvc和gcc中都能正常工作。然而,这个bug只出现在非const情况下,这显然是最常见的用法。我是否做错了什么,还是这真的是一个bug?

https://godbolt.org/z/Gbxjo19Ez

#include <string>
#include <memory>

struct A
{
    // const std::string s; // Oddly, declaring s const compiles
    std::string s;
    constexpr A() = default;
    constexpr A(A&& rh) = default;
    constexpr A& operator=(A&& rh) noexcept
    {
        std::destroy_at(this);
        std::construct_at(this, std::move(rh));
        return *this;
    }
};

constexpr int foo()
{
    A i0{};    // call ctor
    // Fails with clang. OK msvc, gcc
    // construction of subobject of member '_M_local_buf' of union with no active member is not allowed in a constant expression { return ::new((void*)__location) _Tp(std::forward<_Args>(__args)...); }
    i0 = A{};  // call assign rctor
    return 42;
}

int main() {
    constexpr int i = foo();
    return i;
}

对于那些感兴趣的人,这是完整版本,将const对象转换为一级公民(可在向量、排序等中使用)。我非常不喜欢添加getter来维护不变性。

https://godbolt.org/z/hx7f9Krn8


“_M_local_buf”似乎是联合成员中使用的字符串的SSO缓冲区。Libstdc ++似乎没有显式启动此联合子对象的生命周期,而是直接开始复制到其中。如果是通过形如“_M_local_buf [i] = ...”的表达式完成的,这将是可以的,因为这些会按需要启动生命周期,但实际上它会通过引用进行,并且从技术上讲这是未定义的行为。Clang似乎会诊断出这种情况,而GCC则更加宽松。无论如何,它都应该在没有UB的情况下工作,因此这是一种错误。 - user17732522
@user17732522 我注意到gcc有时也会玩得很随意。我曾经在constexpr中看到过reinterpret_cast,但gcc并没有抱怨。考虑到C++的复杂性,大多数情况下都能正常工作真是令人惊讶。然而,多年来在代码中使用它变得更简单/更容易了。我已经记不起上一次遇到内存泄漏编码错误的情况了。 - doug
以下是与无效的活动成员更改相关的GCC错误(bug)链接:https://gcc.gnu.org/bugzilla/show_bug.cgi?id=102286,https://gcc.gnu.org/bugzilla/show_bug.cgi?id=101631。我认为,如果/当它们被修复时,libstdc++也会改变`std::string`的行为。或者在标准中某个时刻可能会放宽活动成员更改的规则。 - user17732522
2个回答

3
是的,这是一个关于libstdc++或clang的问题:std::string的移动构造函数不能在常量表达式中使用。以下代码也会出现相同的错误:
#include <string>

constexpr int f() {
    std::string a;
    std::string b(std::move(a));
    return 42;
}

static_assert(f() == 42);

https://godbolt.org/z/3xWxYW717

https://en.cppreference.com/w/cpp/compiler_support没有显示clang已经支持std::string的constexpr。


谢谢。由于我的原始constexpr代码在声明字符串和整数对象为const时没有在clang中出现问题,我很高兴。然后我想知道C++如何处理从const v非const子对象中窃取内部数据。我怀疑它只是复制它们。结果证明它确实从子对象中窃取了内部数据。因此,在使用const子对象时不会有性能损失。 - doug

0

你的“在原有对象的位置上构造新对象”的游戏是个问题。

  1. 如果对象是const或包含任何const成员子对象,则完全禁止。

由于[basic.life]中的以下规则(请注意,建议在C++17之后的草案中重写此规则)

如果在对象的生命周期结束后,在重新使用或释放对象所占用的存储之前,创建一个新对象并将其放置在原始对象占用的存储位置上,则指向原始对象的指针、引用或原始对象的名称将自动引用新对象,并且一旦新对象的生命周期开始,就可以用来操作新对象,如果满足以下条件:
- 新对象的存储正好覆盖了原始对象占用的存储位置, - 新对象与原始对象是相同类型(忽略顶层cv限定符), - 原始对象的类型没有const限定符,并且(如果是类类型)不包含任何类型为const限定符或引用类型的非静态数据成员, - 原始对象是类型T的最派生对象,而新对象是类型T的最派生对象(即它们不是基类子对象)。

为了实现return *this;和隐式析构函数的目的,你必须遵守这个规则。

  1. constexpr评估期间也不能工作。

...这一点特别针对于std::string小字符串优化可能使用union实现,在constexpr评估期间禁止更改活动union成员,尽管这个规则在C++17之后似乎也已经发生了变化。


1 我认为所谓的更改是错误的(它甚至不允许修复它应该修复的模式),并且会破坏合法的编码模式。虽然指向const限定对象的指针只使得我的视图变为只读,并没有让我假设对象不会被其他持有非限定指针/引用的人更改,但过去如果我得到了一个指向带有const成员的对象的指针(无论是否限定),我就可以确信没有人更改了那个成员,我(或我的优化编译器)可以安全地使用该成员的缓存副本(或从该成员值派生的数据,例如哈希或比较结果)。

显然,这已经不再是真的了。

虽然更改语言规则可能会自动删除所有编译器优化,这些优化会假定const成员是不可变的,但是对于在旧规则下正确且无错误的用户编写的代码,例如使用std::pair<const Key, Value>std::mapstd::unordered_map代码,没有自动补丁。然而,DR似乎没有考虑到这是一个破坏性的更改...

有人要求我提供一个代码片段,以说明现有有效代码的行为变化,这里是。 这段代码以前是非法的,在新规则下它是合法的,但是映射将无法维护其不变性。

std::map<int, T> m{data_source()};
/* new code, now legal */
for( auto& keyvalue : m ) {
    int newkey = -keyvalue.first;
    std::construct_at(&keyvalue.first, newkey);
    // or new (&keyvalue.first) int(newkey);
}
/* existing valid code that breaks */
std::cout << m[some_key()];

考虑限制条件的新放松措辞:
原始对象既不是const限定的完整对象,也不是这样一个对象的子对象。
keyvalue.first被const限定,但它不是完整的对象,而且它是一个完整对象(std::pair)的子对象,该对象没有被const限定。这段代码现在是合法的。它甚至不违反规则的精神,DR明确提到了使用const子对象进行容器元素的就地替换的意图。
std::map的实现以及所有使用map实例的现有代码都会出问题,这是由于现在合法的代码的添加导致的不幸的远程操作。
请注意,实际的键替换可能发生在仅具有指针&keyvalue的代码中,并且不需要知道std::pair实例实际上位于std::map内部,因此正在执行的愚蠢行为不会那么明显。

1
如果我没记错的话,C++20已经发生了变化,这是由于草案上NB评论的结果:https://github.com/cplusplus/nbballot/issues/7 - user17732522
2
自从C++20以来,std::construct_at可以在常量表达式中以这种方式使用。 - user17732522
@cigien:在标准中,我找不到任何其他禁止在迭代映射时原地更改键的规则(解引用迭代器授予对“value_type”的原地引用,该类型是“std :: pair <const Key,Value>”)。 原地更改值部分一直是合法的,但以前禁止原地更改键,这是我强调的规则。 “map”,“unordered_map”和大量使用这些类型的用户代码的实现都依赖于一个假设,即插入后无法更改键。 现在他们可以... - Ben Voigt
由于一个映射对象可以分配给另一个映射对象,因此任何基础常量元素的缓存也可能失败。这在库代码中一直是合法的。然而,它会改变在同一内存位置上的常量内容。似乎必须费些周折才能使基本生活变化成为问题。有人曾经使用放置 new 来更改映射中的常量键元素吗?虽然这是可编译的,但在之前是未定义行为,在现在是合法的,但肯定会搞乱映射。 - doug
不能保证 const map 的键不会在未来的一些赋值操作后更改,但仍具有相同的内存地址。如果 const 对象的生命周期在其间结束,则位于某个位置的 const 对象是不可变的,并且以后不能有另一个值的概念并不成问题。basic.life 的新描述认识到了这一点。 - doug
显示剩余11条评论

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