在编译时填充 std::数组,并使用 const_cast 填充可能的未定义行为
Filling a std::array at compile time and possible undefined behaviour with const_cast
已知自 C++14 以来std::array::operator[]
constexpr
,请参阅下面的声明:
constexpr const_reference operator[]( size_type pos ) const;
但是,它也const
合格。如果要使用std::array
的下标运算符在编译时为数组赋值,这会产生影响。例如,请考虑以下用户文本:
template<typename T, int N>
struct FooLiteral {
std::array<T, N> arr;
constexpr FooLiteral() : arr {} { for(int i(0); i < N; ++i) arr[i] = T{42 + i}; }
};
如果您尝试声明类型 FooLiteral
的 constexpr
变量,上面的代码将无法编译。这是因为重载解析规则将数组下标运算符的非常量限定、非 constexpr 重载限定为更好的匹配。因此,编译器抱怨调用非constexpr
函数。
现场演示
我不知道委员会宣布这种超载const
符合 C++14 条件的原因是什么,但是似乎正在注意到这种影响,并且还有一个提案 p0107R0 在即将到来的 C++17 中解决这个问题。
我克服 C++14 的自然方法是以某种方式破解表达式,以唤起正确的下标运算符。我所做的如下:
template<typename T, int N>
struct FooLiteral {
std::array<T, N> arr;
constexpr FooLiteral() : arr {} {
for(int i(0); i < N; ++i) {
const_cast<T&>(static_cast<const std::array<T, N>&>(arr)[i]) = T{42 + i};
}
}
};
现场演示
也就是说,我将数组强制转换为const
引用以唤起正确的下标运算符重载,然后我将重载的下标运算符的返回对象const_cast
T&
以删除其常量并能够分配给它。
这工作正常,但我知道const_cast
应该谨慎使用,坦率地说,我对这个黑客是否会导致未定义的行为有第二个想法。
直觉上,我认为没有问题,因为这个const_cast
发生在编译时初始化,因此我想不出在这种状态下会出现的含义。
但是是这样,还是我错了,这将UB引入该计划?
问:
有人可以证明这是否是 UB 吗?
据我所知,这不是未定义的行为。将 constexpr 添加到operator[]
的提议发生在从 constexpr 成员函数中删除隐式 const 的更改之前。所以看起来他们只是添加了 constexpr,而没有考虑是否需要保留 const。
我们可以看到一个早期版本的放宽对 constexpr 函数的约束,它对常量表达式中的可变文字说了以下内容:
在常量表达式中创建的对象可以在该常量表达式的计算中修改(包括计算它进行的任何 constexpr 函数调用(,直到该常量表达式的计算结束或对象的生存期结束,以较早发生者为准。它们不能通过以后的常量表达式计算来修改。 [...]
这种方法允许在评估中出现任意变量突变,同时仍保留常量表达评估独立于程序的可变全局状态的基本属性。因此,常量表达式无论何时计算,其计算结果都相同,但未指定值的情况除外(例如,浮点计算可以给出不同的结果,并且通过这些更改,不同的计算顺序也可以给出不同的结果(。
我们可以看到我引用的早期提案指出了const_cast
黑客,它说:
在 C++11 中,constexpr 成员函数是隐式的 const。这为希望能够在常量表达式内部和外部使用的文字类类型带来了问题:
[...]
已经提出了几种替代方法来解决此问题:
- 接受现状,并要求用户用const_cast解决这个小尴尬。
这里没有UB,你的成员arr
是非恒定的,你可以随意"玩"它的const
(嗯,你明白我的意思(
如果你的成员是一个常量表达式,那么你会有 UB,因为你已经在发起者列表中初始化了,并且在创建后,你不允许假设它具有可变值。在初始值设定项列表中执行任何您想要的元编程mumbo jumbo。
不是对问题的直接回答,但希望有用。
在被std::array
困扰了一段时间后,我决定看看是否可以仅使用用户代码做得更好。
事实证明,它可以:
#include <iostream>
#include <utility>
#include <cassert>
#include <string>
#include <vector>
#include <iomanip>
// a fully constexpr version of array that allows incomplete
// construction
template<size_t N, class T>
struct better_array
{
// public constructor defers to internal one for
// conditional handling of missing arguments
constexpr better_array(std::initializer_list<T> list)
: better_array(list, std::make_index_sequence<N>())
{
}
constexpr T& operator[](size_t i) noexcept {
assert(i < N);
return _data[i];
}
constexpr const T& operator[](size_t i) const noexcept {
assert(i < N);
return _data[i];
}
constexpr T* begin() {
return std::addressof(_data[0]);
}
constexpr const T* begin() const {
return std::addressof(_data[0]);
}
constexpr T* end() {
// todo: maybe use std::addressof and disable compiler warnings
// about array bounds that result
return &_data[N];
}
constexpr const T* end() const {
return &_data[N];
}
constexpr size_t size() const {
return N;
}
private:
T _data[N];
private:
// construct each element from the initialiser list if present
// if not, default-construct
template<size_t...Is>
constexpr better_array(std::initializer_list<T> list, std::integer_sequence<size_t, Is...>)
: _data {
(
Is >= list.size()
?
T()
:
std::move(*(std::next(list.begin(), Is)))
)...
}
{
}
};
// compute a simple factorial as a constexpr
constexpr long factorial(long x)
{
if (x <= 0) return 0;
long result = 1;
for (long i = 2 ; i <= x ; result *= i)
++i;
return result;
}
// compute an array of factorials - deliberately mutating a default-
// constructed array
template<size_t N>
constexpr better_array<N, long> factorials()
{
better_array<N, long> result({});
for (long i = 0 ; i < N ; ++i)
{
result[i] = factorial(i);
}
return result;
}
// convenience printer
template<size_t N, class T>
inline std::ostream& operator<<(std::ostream& os, const better_array<N, T>& a)
{
os << "[";
auto sep = " ";
for (const auto& i : a) {
os << sep << i;
sep = ", ";
}
return os << " ]";
}
// for testing non-integrals
struct big_object
{
std::string s = "defaulted";
std::vector<std::string> v = { "defaulted1", "defaulted2" };
};
inline std::ostream& operator<<(std::ostream& os, const big_object& a)
{
os << "{ s=" << quoted(a.s);
os << ", v = [";
auto sep = " ";
for (const auto& s : a.v) {
os << sep << quoted(s);
sep = ", ";
}
return os << " ] }";
}
// test various uses of our new array
auto main() -> int
{
using namespace std;
// quick test
better_array<3, int> x { 0, 3, 2 };
cout << x << endl;
// test that incomplete initialiser list results in a default-constructed object
better_array<2, big_object> y { big_object { "a", { "b", "c" } } };
cout << y << endl;
// test constexpr construction using mutable array
// question: how good is this optimiser anyway?
cout << factorials<10>()[5] << endl;
// answer: with apple clang7, -O3 the above line
// compiles to:
// movq __ZNSt3__14coutE@GOTPCREL(%rip), %rdi
// movl $360, %esi ## imm = 0x168
// callq __ZNSt3__113basic_ostreamIcNS_11char_traitsIcEEElsEl
// so that's pretty good!
return 0;
}
- 在c++中用vector填充一个简单的动态数组
- 如何理解C++标准N3337中的expr.const.cast子句8
- 如何使用用户输入在C++中正确填充2D数组
- 如何找到大小'x'数组是否完全填充,在C++?
- C++Cast运算符过载
- Cuda C++:设备上的Malloc类,并用来自主机的数据填充它
- 通过for循环使用用户输入填充列表
- 根据用户输入用字母填充矢量,并将"开始"和"结束"放在四肢
- 如何正确填充在堆上分配的二维数组?
- 将数字转换为填充字符串
- 有没有办法在一行中填充矢量图
- 用C++中的数字和条件填充向量
- 用真值填充矢量
- 使用结构成员指针在C++中填充结构
- 流填充字符的默认定位
- 使用不同算法的 PKCS1v15 填充进行加密 ++ 签名
- C++:使用缓冲区中的数据填充结构
- 如何将零填充的多维数组传递给 C++ 中的函数?
- Cryptopp:获取密码输入的填充字符串
- 填充上编译器生成的复制构造函数之间的不一致