Члены в функторах constexpr, вызывающие выполнение во время выполнения - PullRequest
0 голосов
/ 18 февраля 2019

Я использую функторы для генерации кода, рассчитанного во время компиляции, следующим образом (я извиняюсь за длинный код, но я нашел единственный способ воспроизвести поведение):

#include <array>
#include <tuple>

template <int order>
constexpr auto compute (const double h)
{
  std::tuple<std::array<double,order>,
         std::array<double,order> > paw{};

  auto xtab = std::get<0>(paw).data();
  auto weight = std::get<1>(paw).data();

  if constexpr ( order == 3 )
              {
            xtab[0] =  - 1.0E+00;
            xtab[1] =    0.0E+00;
            xtab[2] =    1.0E+00;

            weight[0] =  1.0 / 3.0E+00;
            weight[1] =  4.0 / 3.0E+00;
            weight[2] =  1.0 / 3.0E+00;
              }
  else if constexpr ( order == 4 )
              {
            xtab[0] =  - 1.0E+00;
            xtab[1] =  - 0.447213595499957939281834733746E+00;
            xtab[2] =    0.447213595499957939281834733746E+00;
            xtab[3] =    1.0E+00;

            weight[0] =  1.0E+00 / 6.0E+00;
            weight[1] =  5.0E+00 / 6.0E+00;
            weight[2] =  5.0E+00 / 6.0E+00;
            weight[3] =  1.0E+00 / 6.0E+00;
              }

  for (auto & el : std::get<0>(paw))
      el = (el + 1.)/2. * h ;

  for (auto & el : std::get<1>(paw))
    el = el/2. * h ;

  return paw;
}


template <std::size_t n>
class Basis
{
public:

  constexpr Basis(const double h_) :
    h(h_),
    paw(compute<n>(h)),
    coeffs(std::array<double,n>())
  {}

  const double h ;
  const std::tuple<std::array<double,n>,
           std::array<double,n> > paw ;
  const std::array<double,n> coeffs ;

  constexpr double operator () (int i, double x) const
  {
    return 1. ;
  }

}; 

template <std::size_t n,std::size_t p,typename Ltype,typename number=double>
class Functor
{
 public:
  constexpr Functor(const Ltype L_):
    L(L_)
  {}

  const Ltype L ;

  constexpr auto operator()(const auto v) const 
  {
    const auto l = L;
    // const auto l = L();
    std::array<std::array<number,p+1>,p+1> CM{},CM0{},FM{};
    const auto basis = Basis<p+1>(l);
    typename std::remove_const<typename std::remove_reference<decltype(v)>::type>::type w{};

    for (auto i = 0u; i < p + 1; ++i)
      CM0[i][0] += l;
    for (auto i = 0u ; i < p+1 ; ++i)
      for (auto j = 0u ; j < p+1 ; ++j)
        {
          w[i] += CM0[i][j]*v[j];
        }
    for (auto b = 1u ; b < n-1 ; ++b)
      for (auto i = 0u ; i < p+1 ; ++i)
        for (auto j = 0u ; j < p+1 ; ++j)
          {
            w[b*(p+1)+i] += CM[i][j]*v[b*(p+1)+j];
            w[b*(p+1)+i] += FM[i][j]*v[(b+1)*(p+1)+j];
          }
    return w ;
  }
};

int main(int argc,char *argv[])
{
  const auto nel = 4u;
  const auto p = 2u;
  std::array<double,nel*(p+1)> x{} ;
  constexpr auto L = 1.;
  // constexpr auto L = [](){return 1.;};
  const auto A = Functor<nel,p,decltype(L)>(L);
  const volatile auto y = A(x);
  return 0;
}

Iскомпилировать с помощью GCC 8.2.0 с флагами:

-march=native -std=c++1z -fconcepts -Ofast -Wa,-adhln

И при просмотре сгенерированной сборки вычисление выполняется во время выполнения.

Если я изменяю две строки, которые комментируютсядля строк ниже, я обнаружил, что код действительно выполняется во время компиляции, и в сборку помещается только значение переменной volatile.

Я попытался сгенерировать меньший пример, который воспроизводит поведение, ноНебольшие изменения в коде действительно вычисляются во время компиляции.

Я почему-то понимаю, почему предоставление constexpr лямбда-выражения помогает, но я хотел бы понять, почему предоставление double не будет работать в этом случае.В идеале я не хотел бы предоставлять лямбды, потому что это делает мой внешний интерфейс более беспорядочным.

Этот код является частью очень большой кодовой базы, поэтому, пожалуйста, не обращайте внимания на то, что код фактически вычисляет, я создал этот пример, чтобы показатьповедение и не более того.

Какой правильный способ предоставить двойник функтору и сохранить его как const переменную-член без изменения поведения во время компиляции?

Почемунебольшие изменения в функции compute() (например, другие небольшие изменения делают так же) действительно генерируют код времени компиляции?

Я хотел бы понять, каковы фактические условия для GCC, чтобы обеспечить эти компиляциивремя вычислений, так как фактическое приложение, в котором я работаю, требует этого.

Спасибо!

Ответы [ 2 ]

0 голосов
/ 18 февраля 2019
for (auto i = 0u; i < p + 1; ++i)
  CM0[i][0] += l;

когда l является лямбда-типом без сохранения состояния, это преобразует l в тип функции, а затем в bool (целочисленный тип).Это двухэтапное преобразование разрешено, потому что только один «пользовательский».

Это преобразование всегда дает 1 и не зависит от состояния l.

0 голосов
/ 18 февраля 2019

Не уверены, что понимаете, когда ваш код выполняется во время выполнения и когда выполняется во время компиляции, в любом случае правило языка C ++ (не только g ++ и игнорирование правила «как будто») состоит в том, что constexpr функция

  • может быть выполнено во время выполнения и должно выполняться во время выполнения, когда вычисленные значения знают время выполнения (например: значения, поступающие со стандартного ввода)
  • может выполняться во время компиляциии должен выполняться во время компиляции, когда результат направляется туда, где строго необходимо знать значение времени компиляции (например: инициализация constexpr переменной, аргументы шаблона не-типа, измерения массивов в стиле C, static_assert() тесты)
  • есть серая область - когда компилятор знает значение, вовлеченное во время компиляции вычисления, но вычисленное значение не идет, где значение времени компиляции строго требуется - где компилятор может выбрать, если компиляция компиляциивремя или время выполнения.

Если вы заинтересованы в

const volatile auto y = A(x);

, мне кажется, что мы находимся в серой области и комpiler может выбрать, вычислять ли начальное значение для y времени компиляции или времени выполнения.

Если вы хотите y инициализированное время компиляции, я полагаю, вы можете получить это, определяя его (а также предыдущие переменные)) constexpr

  constexpr auto nel = 4u;
  constexpr auto p = 2u;
  constexpr std::array<double,nel*(p+1)> x{} ;
  constexpr auto L = 1.;
  // constexpr auto L = [](){return 1.;};
  constexpr auto A = Functor<nel,p,decltype(L)>(L);
  constexpr volatile auto y = A(x);
...