为什么非const std::array::operator[]不是constexpr

Why is non-const std::array::operator[] not constexpr?

本文关键字:不是 constexpr operator array const std 为什么      更新时间:2023-10-16

我试图在编译时用给定的函数填充一个2D数组。这是我的代码:

template<int H, int W>
struct Table
{
  int data[H][W];
  //std::array<std::array<int, H>, W> data;  // This does not work
  constexpr Table() : data{}
  {
    for (int i = 0; i < H; ++i)
      for (int j = 0; j < W; ++j)
        data[i][j] = i * 10 + j;  // This does not work with std::array
  }
};
constexpr Table<3, 5> table;  // I have table.data properly populated at compile time

它工作得很好,table.data在编译时被正确填充。

然而,如果我用std::array<std::array<int, H>, W>更改普通2D阵列int[H][W],我在循环体中有一个错误:

error: call to non-constexpr function 'std::array<_Tp, _Nm>::value_type& std::array<_Tp, _Nm>::operator[](std::array<_Tp, _Nm>::size_type) [with _Tp = int; long unsigned int _Nm = 3ul; std::array<_Tp, _Nm>::reference = int&; std::array<_Tp, _Nm>::value_type = int; std::array<_Tp, _Nm>::size_type = long unsigned int]'
data[i][j] = i * 10 + j;
^
Compilation failed

很明显,我试图调用std::array::operator[]的非常量重载,它不是constexpr。问题是,为什么它不是constexpr?如果C++14允许我们修改在constexpr作用域中声明的变量,为什么std::array不支持这样做?

我曾经认为std::array就像普通数组一样,只是更好而已。但这里有一个例子,我可以使用纯数组,但不能使用std::array

好的,这确实是标准中的疏忽。甚至有一个解决这个问题的建议:http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/p0107r0.pdf

[N3598]删除了constexpr成员函数作为const的隐式标记。然而std::array的成员函数在这次更改后没有被重新访问,导致了令人惊讶的缺失std::array接口中对constexpr的支持。本文通过添加constexpr到std::array的成员函数,这些函数可以用最少的工作

UPD:在C++17中修复:https://en.cppreference.com/w/cpp/container/array/operator_at

std::array::operator[],因为C++14是constexpr,但也是const限定的:

constexpr const_reference operator[]( size_type pos ) const;
                                                      ^^^^^

因此,您必须强制转换数组以调用正确的operator[]重载:

template<int H, int W>
struct Table
{
  //int data[H][W];
  std::array<std::array<int, H>, W> data;  // This does not work
  constexpr Table() : data{} {
    for (int i = 0; i < W; ++i)
      for (int j = 0; j < H; ++j)
        const_cast<int&>(static_cast<std::array<int, H> const&>(static_cast<std::array<std::array<int, H>, W> const&>(data)[i])[j]) = 10 + j;
  }
};

实时演示

编辑:

与一些人相反,以这种方式使用const_cast并不意味着未定义的行为。事实上,正如放宽constexpr的建议中所提出的那样,用户需要围绕const_cast进行这项工作,以便至少在C++17中解决问题之前引发正确的下标运算符过载(见链接)。

虽然我的第一个想法是"为什么你需要一个非常数组上的constexpr方法"。。。

然后我坐下来写了一个小测试,看看这个想法是否有意义:

#include <iostream>
using namespace std;
struct X{
    constexpr X()
    : _p { 0, 1, 2, 3, 4, 5, 6, 7, 9 }
    {
    }
    constexpr int& operator[](size_t i)
    {
        return _p[i];
    }
    int _p[10];
};
constexpr int foo()
{
    X x;
    x[3] = 4;
    return x[3];
}

auto main() -> int
{
    cout << foo() << endl;
    return 0;
}

事实证明确实如此。

因此,我得出的结论是,委员会采取了与我相同的"显而易见"的观点,并对这个想法不以为然。

在我看来,似乎可以向委员会提出一个建议,在c++17中对其进行修改——以这个问题为例。

这个问题引起了我的极大兴趣,我决定找出一个解决方案,允许在编译时使用一个以x和y为参数的函数初始化数组。

据推测,这可以适用于任何数量的维度。

#include <iostream>
#include <utility>

// function object that turns x and y into some output value. this is the primary predicate
struct init_cell_xy
{
    constexpr init_cell_xy() = default;
    constexpr int operator()(int x, int y) const
    {
        return (1 + x) * (1 + y);
    }
};
// function object that applies y to a given x
template<int X = 1>
struct init_cell_for_x
{
    constexpr init_cell_for_x() = default;
    constexpr int operator()(int y) const
    {
        return _xy(X, y);
    }
private:
    init_cell_xy _xy;
};
// an array of dimension 1, initialised at compile time
template<int Extent>
struct array1
{
    template<class F, int...Is>
    constexpr array1(F&& f, std::integer_sequence<int, Is...>)
    : _values { f(Is)... }
    {}
    template<class F>
    constexpr array1(F&& f = init_cell_for_x<>())
    : array1(std::forward<F>(f), std::make_integer_sequence<int, Extent>())
    {}
    constexpr auto begin() const { return std::begin(_values); }
    constexpr auto end() const { return std::end(_values); }
    constexpr auto& operator[](size_t i) const {
        return _values[i];
    }
private:
    int _values[Extent];
    friend std::ostream& operator<<(std::ostream& os, const array1& s)
    {
        os << "[";
        auto sep = " ";
        for (const auto& i : s) {
            os << sep << i;
            sep = ", ";
        }
        return os << " ]";
    }
};
// an array of dimension 2 - initialised at compile time
template<int XExtent, int YExtent>
struct array2
{
    template<int...Is>
    constexpr array2(std::integer_sequence<int, Is...>)
    : _xs { array1<YExtent>(init_cell_for_x<Is>())... }
    {}
    constexpr array2()
    : array2(std::make_integer_sequence<int, XExtent>())
    {}
    constexpr auto begin() const { return std::begin(_xs); }
    constexpr auto end() const { return std::end(_xs); }
    constexpr auto& operator[](size_t i) const {
        return _xs[i];
    }
private:
    array1<YExtent> _xs[XExtent];
    friend std::ostream& operator<<(std::ostream& os, const array2& s)
    {
        os << "[";
        auto sep = " ";
        for (const auto& i : s) {
            os << sep << i;
            sep = ",n  ";
        }
        return os << " ]";
    }
};


auto main() -> int
{
    using namespace std;
    constexpr array2<6,6> a;
    cout << a << endl;
    return 0;
}