在测试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
。
您的游戏;构造一个新对象来代替旧对象";就是问题所在。
- 如果对象是
const
或包含任何const
成员子对象,则完全禁止
由于[basic.life]
中的以下规则(注意,在C++17之后的草案中建议重写该规则的1)
如果在对象的生存期结束后,在对象所占用的存储被重用或释放之前,在原始对象所占据的存储位置创建了一个新对象,则指向原始对象的指针、引用原始对象的引用或原始对象的名称将自动引用新对象,一旦新对象的生存期开始,就可以用来操作新对象,如果:
- 新对象的存储正好覆盖原始对象所占据的存储位置
和
- 新对象与原始对象的类型相同(忽略顶级cv限定符)
和
- 原始对象的类型不是常量限定的,并且,如果是类类型,则不包含任何类型是常量限定的非静态数据成员或引用类型
和
- 原始对象是类型为
T
的派生程度最高的对象,而新对象是类型T
的派生程度最低的对象(也就是说,它们不是基类子对象)
为了return *this;
和隐式析构函数调用,您必须遵守此规则。
- 它在
constexpr
评估期间也不起作用
。。。这一点是由于std::string
小字符串优化可以使用并集来实现,并且在constexpr评估期间禁止更改活动的并集成员,尽管这一规则在C++17之后似乎也发生了变化。
1我认为上述更改被误导了(它甚至不允许它应该修复的模式),破坏了合法的编码模式。的确,指向const限定对象的指针只会使我的视图只读,并且不会让我假设该对象没有被其他持有不那么限定的指针/引用的人更改,但在过去,如果我被赋予指向具有const
成员的对象的指针(限定或不限定),我得到保证,没有人更改该成员,我(或我的优化编译器)可以安全地使用该成员的缓存副本(或从该成员值派生的数据,如哈希或比较结果)。
显然这已经不是真的了。
虽然更改语言规则可能会自动删除所有假定const
成员不变性的编译器优化,但在旧规则下,用户编写的代码没有正确且无错误的自动补丁,例如使用std::pair<const Key, Value>
的std::map
和std::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
中),因此所做的事情的愚蠢性不会那么明显。