使用const_cast

时间:2015-12-17 15:27:25

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

众所周知,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}; }
};

如果您尝试声明类型为constexpr的{​​{1}}变量,则上述代码将无法编译。这归因于重载决策规则将数组下标运算符的非const限定非constexpr重载限定为更好的匹配。因此编译器抱怨调用非FooLiteral函数。

Live Demo

我无法弄清楚委员会将这个重载声明为constexpr符合C ++ 14的原因是什么原因,但似乎暗示了这个含义并且还有一个提案{{3在upcomming C ++ 17中解决这个问题。

我自然而然地为C ++ 14克服了这个问题,以某种方式破解表达式,以便唤起正确的下标运算符。我做的是以下内容:

const

p0107R0

这就是我将数组转换为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以便删除它的const - 并且能够分配给它。

这很好用,但我知道T&应该谨慎使用,坦率地说,如果这个黑客可以导致未定义的行为,我会再次考虑。

直观地说,我认为没有问题,因为这个const_cast正在编译时进行初始化,因此,我无法想到在这种状态下可能出现的含义。

但是这样,或者我错了,这将UB引入该计划?

问:

有人可以证明这是不是UB?

3 个答案:

答案 0 :(得分:6)

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

我们可以看到表单Relaxing constraints on constexpr functions的早期版本,它表示以下关于在常量表达式中突变文字的内容:

  

在常量表达式中创建的对象可以在对该常量表达式的求值中进行修改(包括对其所做的任何constexpr函数的求值),直到该常量表达式的求值结束,或者对象的生命周期结束为止,无论哪个发生得更快。它们不能通过以后的常量表达式评估来修改。 [...]

     

这种方法允许评估中的任意变量突变,同时仍保留恒定表达评估独立于程序的可变全局状态的基本属性。因此,无论何时计算,常量表达式都会评估为相同的值,除非未指定值(例如,浮点计算可以给出不同的结果,并且通过这些更改,不同的评估顺序也可以给出不同的结果)

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

  

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

     

[...]

     

已提出几种替代方案来解决此问题:

     
      
  • 接受现状,并要求用户使用const_cast处理这种轻微的尴尬。
  •   

答案 1 :(得分:1)

此处没有UB,您的会员arr不是常数,您可以&#34;播放&#34;及其const随意(好吧,你得到我的意思)

如果您的成员 是一个常量表达式,那么您就拥有了UB,因为您已经在initalizer列表中进行了初始化并且创建了帖子,因此您不能认为它具有可变值。在初始化列表中做任何你想要的元编程mumbo jumbo。

答案 2 :(得分:1)

不是问题的直接答案,但希望有用的东西。

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