【问题标题】:Filling a std::array at compile time and possible undefined behaviour with const_cast在编译时填充 std::array 并使用 const_cast 填充可能的未定义行为
【发布时间】:2016-03-24 03:17:17
【问题描述】:

已知std::array::operator[],因为C++14是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 变量,上述代码将无法编译。这归因于重载决策规则将数组下标运算符的非 const 限定、非 constexpr 重载限定为更好的匹配。因此编译器抱怨调用非constexpr 函数。

Live Demo

我不知道委员会将这个重载声明为符合 C++14 条件的 const 的原因是什么,但似乎暗示正在引起注意,还有一个建议 p0107R0 来修复this 在即将到来的 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};
    }
  }
};

Live Demo

也就是说,我将数组转换为 const 引用以唤起正确的下标运算符重载,然后我将 const_cast 重载的下标运算符的返回对象转换为 T&amp; 以删除其 const-ness 并能够分配给它。

这很好用,但我知道应该谨慎使用const_cast,坦率地说,我对这种黑客行为是否会导致未定义的行为有了第二个想法。

直觉上,我认为没有问题,因为这个const_cast 是在编译时初始化时发生的,因此我想不出在这种状态下会出现什么暗示。

但是是这样吗,还是我错了,这将 UB 引入了程序?

问:

有人可以证明这是否是 UB 吗?

【问题讨论】:

    标签: c++ c++14 undefined-behavior constexpr const-cast


    【解决方案1】:

    据我所知,这不是未定义的行为。 that added constexproperator[] 的提议发生在 changes that removed the implicit const from constexpr member functions 之前。所以看起来他们只是在 constexpr 上添加了,没有考虑是否需要保留 const

    我们可以从 Relaxing constraints on constexpr functions 的早期版本中看到,它对常量表达式中的文字变化做了以下说明:

    在常量表达式中创建的对象可以在该常量表达式的求值过程中进行修改(包括对其进行的任何 constexpr 函数调用的求值),直到该常量表达式的求值结束或对象的生命周期结束,以两者为准发生得更快。它们不能被以后的常量表达式求值修改。 [...]

    这种方法允许在求值过程中进行任意变量突变,同时仍保留常量表达式求值独立于程序的可变全局状态这一基本属性。因此,无论何时计算常量表达式,都会得到相同的值,但未指定值时除外(例如,浮点计算可以给出不同的结果,并且随着这些变化,不同的求值顺序也可以给出不同的结果) .

    我们可以看到我引用的早期提案指出了const_cast hack,它说:

    在 C++11 中,constexpr 成员函数是隐式 const。这给希望在常量表达式内部和外部都可用的文字类类型带来了问题:

    [...]

    已经建议了几种替代方案来解决此问题:

    • 接受现状,并要求用户使用 const_cast 解决这个小尴尬。

    【讨论】:

      【解决方案2】:

      这里没有 UB,你的成员 arr 不是固定的,你可以随意“玩”它的 constness(嗯,你明白我的意思)

      如果您的成员一个常量表达式,那么您将拥有 UB,因为您已经在初始化器列表中进行了初始化并且创建后您不能假定它具有可变值。在初始化列表中做任何你想做的元编程。

      【讨论】:

      • 看来你说OP不能用它来初始化一个constexpr FooLiteral,至于const FooLiteral,不能修改成员...
      【解决方案3】:

      不是对这个问题的直接回答,但希望有一些有用的东西。

      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;
      }
      

      【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-30
      • 1970-01-01
      • 2019-03-24
      • 2019-07-12
      相关资源
      最近更新 更多