简体   繁体   English

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

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

It is known that std::array::operator[] since C++14 is constexpr , see declaration below: 众所周知,自C ++ 14以来std::array::operator[]constexpr ,参见下面的声明:

constexpr const_reference operator[]( size_type pos ) const; 

However, it is also const qualified. 但是,它也是const限定的。 This causes implications if you want to use the subscript operator of a std::array in order to assign values to your array at compile time. 如果要使用std::array的下标运算符,以便在编译时为数组赋值,则会产生影响。 For example consider the following user literal: 例如,考虑以下用户文字:

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

The above code won't compile if you try to declare a constexpr variable of type FooLiteral . 如果您尝试声明类型为FooLiteralconstexpr变量,则上述代码将无法编译。 This is attributed to the fact that overload resolution rules qualify the non-const qualified, non-constexpr overload of the array's subscript operator as a better match. 这归因于重载决策规则将数组下标运算符的非const限定非constexpr重载限定为更好的匹配。 Thus the compiler complains about calling a non- constexpr function. 因此编译器抱怨调用非constexpr函数。

Live Demo 现场演示

I can't figure out what was the reason for the commitee to declare this overload as const qualified for C++14, however it seems that the implication is being noticed and there's also a proposal p0107R0 to fix this in the upcomming C++17. 我无法弄清楚委员会将这个重载声明为const限定为C ++ 14的原因是什么,但似乎有人注意到了这个含义,并且还有一个提议p0107R0来解决这个问题在升级C ++中17。

My natural though to overcome this for C++14 was to somehow hack the expression, in order to evoke the correct subscript operator. 我自然而然地为C ++ 14克服这个问题是为了唤起正确的下标运算符,以某种方式破解表达式。 What I did is the following: 我做的是以下内容:

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 现场演示

That is I casted the array to const reference to evoke the correct subscript operator overload and then I const_cast the returned object of the overloaded subscript operator to T& in order remove its const-ness and be able to assign to it. 这就是我将数组转换为const引用以唤起正确的下标运算符重载,然后我将重载的下标运算符的返回对象const_castT& ,以便删除它的常量并能够分配给它。

This works fine, but I know that const_cast should be used with caution and to be frank I have second thoughts about if this hack can cause undefined behaviour. 这工作正常,但我知道const_cast应该谨慎使用,坦率地说,我有第二个想法,如果这个黑客可以导致不明确的行为。

Intuitively, I don't think there's a problem, since this const_cast is taking place at compile time initialization thus, I can't think of an implication that can arise at this state. 直觉上,我认为没有问题,因为这个const_cast是在编译时初始化时发生的,因此,我无法想到在这种状态下可能出现的含义。

But is that so, or am I wrong and this introduce UB to the program? 但是这样,或者我错了,这将UB引入该计划?

Q: 问:

Can someone justify if this is a UB or not? 如果这是一个UB,有人可以证明这一点吗?

As far as I can tell this is not undefined behavior. 据我所知,这不是未定义的行为。 The proposal that added constexpr to operator[] happened before the changes that removed the implicit const from constexpr member functions . constexpr添加operator[]的提议发生在从constexpr成员函数中删除隐式const更改之前。 So it looks like they just added on constexpr without reflecting on the need for keeping const or not. 所以看起来他们只是添加了constexpr而没有反映是否需要保持const

We can see form an earlier version of Relaxing constraints on constexpr functions that it says the following about mutating literals within a constant expression: 我们可以在constexpr函数中看到一个早期版本的Relax约束 ,它说明了在常量表达式中改变文字的内容:

Objects created within a constant expression can be modified within the evalution of that constant expression (including the evaluation of any constexpr function calls it makes), until the evaluation of that constant expression ends, or the lifetime of the object ends, whichever happens sooner. 在常量表达式中创建的对象可以在对该常量表达式的求值中进行修改(包括对其所做的任何constexpr函数的求值),直到该常量表达式的求值结束,或者对象的生命周期结束,以较早者为准。 They cannot be modified by later constant expression evaluations. 它们不能通过以后的常量表达式评估来修改。 [...] [...]

This approach allows arbitrary variable mutations within an evaluation, while still preserving the essential property that constant expression evaluation is independent of the mutable global state of the program. 这种方法允许评估中的任意变量突变,同时仍保留恒定表达评估独立于程序的可变全局状态的基本属性。 Thus a constant expression evaluates to the same value no matter when it is evaluated, excepting when the value is unspecified (for instance, floating-point calculations can give different results and, with these changes, differing orders of evaluation can also give different results). 因此,无论何时计算,常量表达式都会评估为相同的值,除非未指定值(例如,浮点计算可以给出不同的结果,并且通过这些更改,不同的评估顺序也可以给出不同的结果) 。

and we can see the earlier proposal I referenced points out the const_cast hack and it says: 我们可以看到我引用的早期提案指出了const_cast hack,它说:

In C++11, constexpr member functions are implicitly const. 在C ++ 11中,constexpr成员函数是隐式const。 This creates problems for literal class types which desire to be usable both within constant expressions and outside them: 这会为希望在常量表达式和外部表达式中使用的文字类类型带来问题:

[...] [...]

Several alternatives have been suggested to resolve this problem: 已经提出了几种替代方案来解决这个问题:

  • Accept the status quo, and require users to work around this minor embarrassment with const_cast. 接受现状,并要求用户使用const_cast解决这个轻微的尴尬。

No UB here, your member arr is non constant, you can "play" with its const ness at will (well sort of, you get what I mean) 没有UB在这里,你的成员arr是非常数,你可以随意“玩”它的const (好吧,你明白我的意思)

If your member was a constant expression then you'd have UB, because you have already initialized in the initalizer list and post creation you are not allowed to assume it has mutable value. 如果你的成员一个常量表达式,那么你就有了UB,因为你已经在initalizer列表中进行了初始化,并且在创建后你不能认为它具有可变值。 Do whatever metaprogramming mumbo jumbo you wish inside the initializer list. 在初始化列表中做任何你想要的元编程mumbo jumbo。

Not a direct answer to the question but hopefully something useful. 不是问题的直接答案,但希望有用的东西。

Having been troubled by std::array for a while I decided to see if it can be done better using user-code only. std::array困扰了一段时间之后,我决定看看它是否可以仅使用用户代码做得更好。

Turns out it can: 原来它可以:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM