web-dev-qa-db-fra.com

C++ "oubliant" cette variable est constexpr lorsqu'elle est utilisée comme argument de fonction

J'ai le code suivant où je suis irrité par le fait que le compilateur est incapable de voir cette variable passée en argument à une fonction est constexpr donc je dois utiliser la fonction arity 0 au lieu de 1 argument.

Je sais que ce n’est pas un bug du compilateur, mais je me demande s’il existe des idiomes permettant de contourner ce problème.

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

Si ce que je veux n’est pas évident: j’aimerais modifier la signature get_evens de sorte qu’il s’agisse d’un modèle basé sur la taille du tableau N et qu’il prenne 1 argument de type const std::array<int, N>&.

Le message d'erreur lorsque je change arr en argument de fonction n'est pas utile: 

prog.cc:25:21: remarque: l'initialiseur de 'cnt' n'est pas une expression constante prog.cc:19:19: remarque: déclaré ici constexpr int cnt = [&arr, &is_even]()constexpr {

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

this fonctionne dans g ++ , mais dans Clang nous rencontrons un problème car le begin sur une array n'est pas correctement constexpr avec au moins une bibliothèque . Ou peut-être que g ++ viole le standard et que le son ne le fait pas.

1

Un argument de fonction n'est jamais une expression constante, même si une fonction est utilisée dans le contexte constexpr:

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

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

Pour imiter un argument constexpr, utilisez un paramètre de modèle:

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

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

Une solution possible consiste à utiliser std::integer_sequence pour coder des entiers dans un type:

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

Ajout suggéré par Constantinos Glynos.

From Effective Modern C++ livre de Scott Meyers , point 15, p.98:

  • Les fonctions constexpr peuvent être utilisées dans des contextes exigeant des constantes à la compilation. Si les valeurs des arguments que vous transmettez à une fonction constexpr dans un tel contexte sont connues lors de la compilation, le résultat sera calculé lors de la compilation. Si aucune des valeurs des arguments n’est connue lors de la compilation, votre code sera rejeté. 
  • Lorsqu'une fonction constexpr est appelée avec une ou plusieurs valeurs inconnues lors de la compilation, elle agit comme une fonction normale et en calcule le résultat au moment de l'exécution. Cela signifie que vous n’avez pas besoin de deux fonctions pour effectuer la même opération, une pour les constantes à la compilation et une pour toutes les autres valeurs. La fonction constexpr fait tout.
9
Evg

L'autre réponse fonctionne correctement, mais je pense que le raisonnement n'a rien à voir avec les paramètres mais plutôt avec la capture lambda ici:

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

En effet, nous pouvons tester les différents scénarios avec ce code:

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

Notez que la ligne où test3 est initialisé ne se compile pas. Ceci, cependant, compile très bien:

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

Alors, quel est le problème ici? Bien les lambdas ne sont que des foncteurs glorifiés, et à l’intérieur cela ressemblera à quelque chose comme ça:

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

Notez maintenant que nous obtenons un message d'erreur montrant le vrai problème:

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

L'autre réponse cite le livre de Scotts Meyer mais interprète mal les citations. Le livre présente en fait plusieurs exemples de paramètres utilisés dans des situations constexpr, mais les guillemets indiquent simplement que si vous transmettez un paramètre non-constexpr, la fonction peut être exécutée au moment de la compilation.

2
user10506568

En suivant la suggestion de Evg, en passant ainsi les nombres comme paramètres de modèle d'un std::integer_sequence, mais en transmettant la séquence entière en tant qu'argument de la fonction get_evens(), et non en tant que paramètre de modèle, vous pouvez utiliser les nombres directement à l'intérieur de get_evens().

Je veux dire ... vous pouvez simplifier la get_evens() comme suit (EDIT: encore simplifié à la suite d’une suggestion de Evg (Merci!))

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

et vous pouvez l'utiliser de cette façon

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;
 }
2
max66
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) >{};
}

Exemple live .

Code de test:

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