C++17静态模板惰性求值

igsr9ssn  于 2023-02-01  发布在  其他
关注(0)|答案(4)|浏览(248)

考虑以下示例:

#include<iostream>

template<int n>
struct fibonacci {
    static const int value = n < 0 ? 0 : fibonacci<n-1>::value + fibonacci<n-2>::value;
};

template<>
struct fibonacci<1> {
    static const int value = 1;
};

template<>
struct fibonacci<0> {
    static const int value = 0;
};

int main() {
    
    std::cout << fibonacci<-1>::value << std::endl;

    return 0;
}

我对C++中的惰性求值很熟悉,当传递一个〈0的参数时,我希望泛型fibonacci模板的if语句的第二个分支不会被求值,然而,编译代码仍然会导致从该分支开始的无限循环:

Fibonacci.cpp: In instantiation of ‘const int fibonacci<-900>::value’:
Fibonacci.cpp:5:58:   recursively required from ‘const int fibonacci<-2>::value’
Fibonacci.cpp:5:58:   required from ‘const int fibonacci<-1>::value’
Fibonacci.cpp:20:33:   required from here
Fibonacci.cpp:5:58: fatal error: template instantiation depth exceeds maximum of 900 (use ‘-ftemplate-depth=’ to increase the maximum)
    5 |     static const int value = n < 0 ? 0 : fibonacci<n-1>::value + fibonacci<n-2>::value;
      |                                                          ^~~~~

为什么会这样呢?是特定于模板化结构体的吗?

vxf3dgd4

vxf3dgd41#

我对C++中的惰性求值很熟悉,并且希望在传递参数〈0时,if语句的第二个分支(泛型fibonacci模板)不会被求值。
它不需要求值,但我们这里不处理求值,我们处理的是模板示例化,你使用了fibonacci<n-1>::value,这需要示例化完整的对象类型fibonacci<n-1>,必须检查该类型,看看它是否有成员value可以在这样的表达式中使用。
示例化一个类模板会导致其成员的声明被示例化。静态数据成员的声明包含一个初始化器,因此它也必须被示例化。所以我们需要递归地示例化模板。
简单地命名fibonacci<n-1>不会导致它被示例化(向前看声明),如果你想延迟示例化,你必须延迟使用那些需要定义的类型(比如访问成员)。
这方面的老的元编程技巧(这与函数式编程非常一致)涉及到帮助器模板。

template<class L, class R>
struct add {
    static constexpr auto value = L::value + R::value;
};

template<int n>
struct fibonacci {
    static const int value = std::conditional_t<(n < 0), fibonacci<0>, add<fibonacci<n-1>, fibonacci<n-2>>>::value;
};

std::conditional_t将根据条件选择一个类型。然后,访问该类型(并且仅访问该类型)的::value。因此,直到实际需要时,才完全示例化任何内容。

bybem2ql

bybem2ql2#

您可以使用if constexpr

template<int n>
struct fibonacci {
    static const int value = []() {
        if constexpr (n < 0) {
            return 0;
        } else {
            return fibonacci<n-1>::value + fibonacci<n-2>::value;
        }
    }();
};
pqwbnv8z

pqwbnv8z3#

fibonaccin的某个值示例化时,在该示例化中使用的所有表达式也必须被编译。这意味着使用的任何模板也必须被示例化。即使包含模板示例化的表达式从未被求值,这也是必要的。
避免在表达式中示例化模板的唯一方法是根本不编译表达式,这样可以避免用不正确的参数示例化模板。
您可以通过使用C++17的if constexpr来完成此操作:

template<int n>
struct fibonacci {
    static const int value = [] {
      if constexpr (n < 0) return 0;
       else return fibonacci<n-1>::value + fibonacci<n-2>::value;
    }();
};

这是一个demo

xpcnnkqh

xpcnnkqh4#

ArtefactoStory Teller - Unslander Monica已经提供了正确的答案。
我只是想借助cppinsights进一步解释
为了便于解释,请考虑稍作修改的原始代码:

...

template<int n>
struct fibonacci {
    static const int value = n > 0 ? 0 : fibonacci<n-1>::value + fibonacci<n-2>::value;
};

...
int main() {
    
    std::cout << fibonacci<3>::value << std::endl; // modified parameter from -1 to 3 to avoid compilation failure by recursive instantiation

    return 0;
}

下面是编译器生成的代码的外观

...
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct fibonacci<3>
{
  static const int value = 3 > 0 ? 0 : fibonacci<2>::value + fibonacci<1>::value;
};

#endif

/* First instantiated from: insights.cpp:5 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct fibonacci<2>
{
  static const int value = 2 > 0 ? 0 : fibonacci<1>::value + fibonacci<0>::value;
};

#endif
...
int main()
{
  std::cout.operator<<(fibonacci<2>::value).operator<<(std::endl);
  return 0;
}

从上面生成的代码中可以清楚地看到,即使表达式3 > 0为真,编译器仍然为n = 2n = 1示例化false表达式中的模板
现在来看一下Artefacto共享的if constexpr代码。

template<int n>
struct fibonacci {
    static const int value = []() {
        if constexpr (n < 0) {
            return 0;
        } else {
            return fibonacci<n-1>::value + fibonacci<n-2>::value;
        }
    }();
};

将模板参数视为-1。下面是编译器将如何解释它

#ifdef INSIGHTS_USE_TEMPLATE
template<>
struct fibonacci<-1>
{
      
  class __lambda_5_30
  {
    public: 
    inline /*constexpr */ int operator()() const
    {
      if constexpr(-1 < 0) {
        return 0;
      }
      
    }
    
    using retType_5_30 = int (*)();
    inline /*constexpr */ operator retType_5_30 () const noexcept
    {
      return __invoke;
    };
    
    private: 
    static inline int __invoke()
    {
      if constexpr(-1 < 0) {
        return 0;
      }
      
    }
    
    
  } __lambda_5_30{};
  
  static const int value = __lambda_5_30.operator()();
  
  class __anon_5_30;
};

#endif

从上面的代码中可以清楚地看到编译器甚至没有考虑if constexpr的else部分,这就是为什么这段代码可以完全正常地工作。

相关问题