当从构造函数中分配包含"std::string"的r值时,clang中存在明显的错误



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

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

是的,这是一个libstdc++或clang问题:std::string的move构造函数不能在常量表达式中使用。以下给出了相同的错误:

#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支持constexprstd::string

您的游戏;构造一个新对象来代替旧对象";就是问题所在。

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

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

如果在对象的生存期结束后,在对象所占用的存储被重用或释放之前,在原始对象所占据的存储位置创建了一个新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象,一旦新对象的生存期开始,就可以用来操作新对象,如果:

  • 新对象的存储正好覆盖原始对象所占据的存储位置

  • 新对象与原始对象的类型相同(忽略顶级cv限定符)

  • 原始对象的类型不是常量限定的,并且,如果是类类型,则不包含任何类型是常量限定的非静态数据成员或引用类型

  • 原始对象是类型为T的派生程度最高的对象,而新对象是类型T的派生程度最低的对象(也就是说,它们不是基类子对象)

为了return *this;和隐式析构函数调用,您必须遵守此规则。

  1. 它在constexpr评估期间也不起作用

。。。这一点是由于std::string小字符串优化可以使用并集来实现,并且在constexpr评估期间禁止更改活动的并集成员,尽管这一规则在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限定的,但它不是一个完整对象,并且它是一个不符合const的完整对象(std::pair<const Key, Value>)的子对象此代码现在是合法的。这甚至不违反规则的精神,DR明确提到了用const子对象执行容器元素的就地替换的意图。

std::map的实现与所有使用映射实例的现有代码一起,打破了由于添加了现在合法的代码而导致的不幸的操作。

请注意,密钥的实际替换可能发生在只有指针&keyvalue的代码中,而不必知道std::pair实例实际上在std::map中),因此所做的事情的愚蠢性不会那么明显。

最新更新