在编译时填充 std::数组,并使用 const_cast 填充可能的未定义行为

Filling a std::array at compile time and possible undefined behaviour with const_cast

本文关键字:填充 cast 未定义 const std 编译 数组      更新时间:2023-10-16

已知自 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}; }
};

如果您尝试声明类型 FooLiteralconstexpr 变量,上面的代码将无法编译。这是因为重载解析规则将数组下标运算符的非常量限定、非 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;
}