C++ "forgetting" 该变量在用作函数参数时是 constexpr

C++ "forgetting" that variable is constexpr when used as function argument

我有以下代码,编译器无法看到作为参数传递给函数的变量是 constexpr,这让我很恼火,所以我必须使用 arity 0 函数而不是 1 参数函数。

我知道这不是编译器错误,但我想知道是否有可以解决此问题的习惯用法。

#include <array>
#include <iostream>

static constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

template <typename C, typename P, typename Y>
static constexpr void copy_if(const C& rng, P p, Y yi3ld) {
    for (const auto& elem: rng) {
        if (p(elem)){
            yi3ld(elem);
        }
    }
}

// template<std::size_t N>
static constexpr auto get_evens(/* const std::array<int, N>& arr */) {
    constexpr auto is_even = [](const int i) constexpr {return i % 2 == 0;};
    constexpr int cnt = [/* &arr, */&is_even]() constexpr {
        int cnt = 0;
        auto increment = [&cnt] (const auto&){cnt++;};
        copy_if(arr, is_even, increment);
        return cnt;
    }();
    std::array<int, cnt> result{};
    int idx = 0;
    copy_if(arr, is_even, [&result, &idx](const auto& val){ result[idx++] = val;});
    return result;
}

int main() {
    // constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    for (const int i:get_evens(/* arr */)) {
        std::cout << i << " " << std::endl;
    }
}

如果我想要的不是很明显:我想更改 get_evens 签名,以便它是基于数组大小 N 的模板模板,并且它需要 1 个 const std::array<int, N>& 类型的参数。

当我将 arr 更改为函数参数时出现的错误消息没有帮助:

prog.cc:25:21: note: initializer of 'cnt' is not a constant expression prog.cc:19:19: note: declared here constexpr int cnt = [&arr, &is_even]()constexpr {

函数参数永远不是常量表达式,即使在 constexpr 上下文中使用函数也是如此:

constexpr int foo(int i)
{
    // i is not a constexpr
    return i + 1;
}

constexpr auto i = 1;
constexpr auto j = foo(i);    

要模仿 constexpr 参数,请使用模板参数:

template<int i>
constexpr int foo()
{
    // i is constexpr
    return i + 1;
}

constexpr auto i = 1;
constexpr auto j = foo<i>();

一种可能的解决方案是使用 std::integer_sequence 将整数编码为一种类型:

#include <array>
#include <iostream>
#include <type_traits>

template<typename P, typename Y, int... elements>
constexpr void copy_if_impl(P p, Y yi3ld, std::integer_sequence<int, elements...>) {
    ((p(elements) && (yi3ld(elements), true)), ...);
}

template<typename arr_t, typename P, typename Y>
constexpr void copy_if(P p, Y yi3ld) {
    copy_if_impl(p, yi3ld, arr_t{});
}

template<typename arr_t>
constexpr auto get_evens(){
    constexpr auto is_even = [](const int i) constexpr { return i % 2 == 0; };
    constexpr int cnt = [&is_even]() constexpr {
        int cnt = 0;
        auto increment = [&cnt](const auto&) { cnt++; };
        copy_if<arr_t>(is_even, increment);
        return cnt;
    }();

    std::array<int, cnt> result{};
    int idx = 0;
    copy_if<arr_t>(is_even, [&result, &idx](const auto& val) {
        result[idx++] = val; });
    return result;
}

int main()
{
    using arr = std::integer_sequence<int, 11, 22, 33, 44, 55>;
    for (const int i : get_evens<arr>()) {
        std::cout << i << " " << std::endl;
    }
}

Constantinos Glynos 建议添加。

来自 Effective Modern C++Scott Meyers,第 15 项,第 98 页:

  • constexpr functions can be used in contexts that demand compile-time constants. If the values of the arguments you pass to a constexpr function in such a context are known during compilation, the result will be computed during compilation. If any of the arguments’ values is not known during compilation, your code will be rejected.
  • When a constexpr function is called with one or more values that are not known during compilation, it acts like a normal function, computing its result at runtime. This means you don’t need two functions to perform the same operation, one for compile-time constants and one for all other values. The constexpr function does it all.

另一个答案有一个正确的解决方法,但我认为推理与参数无关,而是与此处的 lambda 捕获有关:

constexpr int cnt = [/* &arr, */&is_even]() 

确实我们可以用这段代码测试各种场景:

#include <array> 
#include <iostream>

template <size_t N>
constexpr int foo(const std::array<int, N>& arr) {
    return [&arr] () { return arr.size(); }();
}

template <size_t N>
constexpr int bar(const std::array<int, N>& arr) {
    int res{};
    for (auto i : arr) {
        res++;
    }
    return res;
}

template <size_t N>
constexpr int baz(const std::array<int, N>& arr)     {
    constexpr int test = [&arr] () constexpr {
        return bar(arr);
    }();
    return test;
}

int main() {
    constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    constexpr std::array<int, foo(arr)> test{};
    constexpr std::array<int, bar(arr)> test2{};
    constexpr std::array<int, baz(arr)> test3{};
}   

请注意,test3 初始化的行编译失败。然而,这编译得很好:

template <size_t N>
constexpr int baz(const std::array<int, N>& arr) {
    return bar(arr);
}

那么,这里有什么问题?好吧,lambda 真的只是美化了的仿函数,在内部它看起来像这样:

struct constexpr_functor {
    const std::array<int, 5>& arr;
    constexpr constexpr_functor(const std::array<int, 5>& test)
        : arr(test) { }
    constexpr int operator()() const {
        return bar(arr);
    }
};
// ...
constexpr constexpr_functor t{arr};
constexpr std::array<int, t()> test3{};

现在请注意,我们收到一条显示真正问题的错误消息:

test.cpp:36:33: note: reference to 'arr' is not a constant expression
test.cpp:33:34: note: declared here
    constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

另一个答案引用了 Scotts Meyer 的书,但误解了引文。这本书实际上展示了在 constexpr 情况下使用参数的几个示例,但引号只是说如果你传递一个非 constexpr 参数,该函数可以 运行 在编译时。

按照 Evg 的建议,将数字作为 std::integer_sequence 的模板参数传递,但将整数序列作为 get_evens() 函数的参数传递,而不是模板参数,您可以使用get_evens().

中的数字

我的意思是...您可以按如下方式简化 get_evens()编辑:根据 Evg 的建议进一步简化(谢谢!))

template <typename T, T ... Ts>
constexpr auto get_evens (std::integer_sequence<T, Ts...> const &)
 {
   std::array<T, (std::size_t(!(Ts & T{1})) + ...)> result{};

   std::size_t idx = 0;

   ((void)(Ts & 1 || (result[idx++] = Ts, true)), ...);

   return result;
 } 

你可以这样使用它

int main()
 {
   using arr = std::integer_sequence<int, 11, 22, 33, 44, 55>;

   for ( const int i : get_evens(arr{}) )
      std::cout << i << " " << std::endl;
 }
#include <array>
#include <iostream>

static constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};

template <typename C, typename P, typename T>
static constexpr void invoke_if(const C& rng, P p, T target) {
    for (const auto& elem: rng) {
        if (p(elem)){
            target(elem);
        }
    }
}

constexpr bool is_even(int i) {
    return i % 2 == 0;
}

template<std::size_t N>
constexpr std::size_t count_evens(const std::array<int, N>& arr)
{
    std::size_t cnt = 0;
    invoke_if(arr, is_even, [&cnt](auto&&){++cnt;});
    return cnt;
}

template<std::size_t cnt, std::size_t N>
static constexpr auto get_evens(const std::array<int, N>& arr) {
    std::array<int, cnt> result{};
    int idx = 0;
    invoke_if(arr, is_even, [&result, &idx](const auto& val){ result[idx++] = val;});
    return result;
}

int main() {
    // constexpr std::array<int, 5> arr{11, 22, 33, 44, 55};
    for (const int i:get_evens<count_evens(arr)>(arr)) {
        std::cout << i << " " << std::endl;
    }
}

这个works in g++, but in clang we get a problem because the begin on an array isn't properly constexpr with at least one library。或者可能 g++ 违反了标准而 clang 没有。

template<auto t0, auto...ts>
struct ct_array:
  std::array<decltype(t0) const, 1+sizeof...(ts)>,
  std::integer_sequence<decltype(t0), t0, ts...>
{
  ct_array():std::array<decltype(t0) const, 1+sizeof...(ts)>{{t0, ts...}} {};
};

template<class target, auto X>
struct push;
template<auto X>
struct push<void, X>{using type=ct_array<X>;};
template<auto...elems, auto X>
struct push<ct_array<elems...>, X>{using type=ct_array<elems...,X>;};
template<class target, auto X>
using push_t= typename push<target, X>::type;

template<class target>
struct pop;
template<auto x>
struct pop<ct_array<x>>{using type=void;};
template<auto x0, auto...xs>
struct pop<ct_array<x0, xs...>>{using type=ct_array<xs...>;};
template<class target>
using pop_t=typename pop<target>::type;

template<class lhs, class rhs, class F, class=void>
struct transcribe;
template<class lhs, class rhs, class F>
using transcribe_t = typename transcribe<lhs, rhs, F>::type;

template<auto l0, auto...lhs, class rhs, class F>
struct transcribe<ct_array<l0, lhs...>, rhs, F,
  std::enable_if_t<F{}(l0) && sizeof...(lhs)>
>:
  transcribe<pop_t<ct_array<l0, lhs...>>, push_t<rhs, l0>, F>
{};
template<auto l0, auto...lhs, class rhs, class F>
struct transcribe<ct_array<l0, lhs...>, rhs, F,
  std::enable_if_t<!F{}(l0) && sizeof...(lhs)>
>:
  transcribe<pop_t<ct_array<l0, lhs...>>, rhs, F>
{};
template<auto lhs, class rhs, class F>
struct transcribe<ct_array<lhs>, rhs, F, void>
{
  using type=std::conditional_t< F{}(lhs), push_t<rhs, lhs>, rhs >;
};
template<class lhs, class F>
using filter_t = transcribe_t<lhs, void, F>;

// C++20
//auto is_even = [](auto i)->bool{ return !(i%2); };
struct is_even_t {
  template<class T>
  constexpr bool operator()(T i)const{ return !(i%2); }
};
constexpr is_even_t is_even{};

template<auto...is>
static constexpr auto get_evens(ct_array<is...>) {
  return filter_t< ct_array<is...>, decltype(is_even) >{};
}

Live example.

测试代码:

auto arr = ct_array<11, 22, 33, 44, 55>{};
for (const int i : get_evens(arr)) {
    std::cout << i << " " << std::endl;
}