Video Link: C++ Weekly - Ep 18 C++17’s constexpr if

Verification Case

constexpr if用于在编译期进行分支选择,不满足条件的分支在编译期被丢弃。从某种程度上来说,constexpr if有着#if...#else类似的效果。举例来看dynamic_libraryinvoke函数要同时支持返回值是void以及其他的场景:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  template <typename RetT, typename... Args>
  auto invoke(CCString name, Args&&... args) const
    -> std::enable_if_t<!std::is_void_v<RetT>, std::optional<RetT>> {
    auto func = load<RetT, Args...>(name);
    if (!func.has_value()) {
      return std::nullopt;
    }
    return std::invoke(func.value(), std::forward<Args>(args)...);
  }

  template <typename RetT, typename... Args>
  auto invoke(CCString name, Args&&... args) const
  -> std::enable_if_t<std::is_void_v<RetT>, bool> {
    auto func = load<RetT, Args...>(name);
    if (func.has_value()) {
      std::invoke(func.value(), std::forward<Args>(args)...);
      return true;
    }
    return false;
  }

基于c++当前并未支持对void返回值的函数进行return,同时std::optional也无void特化场景,所以针对两种返回值类别的函数,可以使用SFINAE技巧设计为两个实现。但此处的重复代码之多也是令人心烦,使用constexpr if可以优化如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  template <typename RetT, typename... Args>
  auto invoke(CCString name, Args&&... args) const {
    auto func = load<RetT, Args...>(name);
    if constexpr (!std::is_void_v<RetT>) {
      if (!func.has_value()) {
        return std::optional<RetT>();
      }
      return std::make_optional(
        std::invoke(func.value(), std::forward<Args>(args)...));
    } else {
      if (func.has_value()) {
        std::invoke(func.value(), std::forward<Args>(args)...);
        return true;
      }
      return false;
    }
  }

cppinsights使用c++17进行对invoke<int, int, int>进行展开:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
    #ifdef INSIGHTS_USE_TEMPLATE
    template<>
    inline std::optional<int> invoke<int, int, int>(CCString name, int && __args1, int && __args2) const
    {
      std::optional<int (*)(int, int)> func = this->load<int, int, int>(name);
      if constexpr(!std::is_void_v<int>) {
        if(!static_cast<const std::__optional_storage_base<int (*)(int, int), false>&>(func).has_value()) {
          return std::optional<int>();
        }
        
        return std::make_optional(std::invoke(func.value(), std::forward<int>(__args1), std::forward<int>(__args2)));
      }
      
    }
    #endif

else分支已被丢弃,只剩下常true的分支。

当然,以上代码的重复度依然非常惨烈,同时可读性也未必比SFINAE好,同时std::optionalreturn写法也比较啰嗦。如此先用std::conditional解决return std::optional无法使用std::nullopt的场景,以及需要显式使用std::make_optional的麻烦:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
  template <typename RetT, typename... Args>
  auto invoke(CCString name, Args&&... args) const
  -> std::conditional_t<!std::is_void_v<RetT>, std::optional<RetT>, bool> {
    auto func = load<RetT, Args...>(name);
    if constexpr (!std::is_void_v<RetT>) {
      if (!func.has_value()) {
        return std::nullopt;
      }
      return std::invoke(func.value(), std::forward<Args>(args)...);
    } else {
      if (func.has_value()) {
        std::invoke(func.value(), std::forward<Args>(args)...);
        return true;
      }
      return false;
    }
  }

std::invoke由于返回值void的关系暂无力解决重复,而if (!func.has_value())还有机会:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
  template <typename RetT, typename... Args>
  auto invoke(CCString name, Args&&... args) const
  -> std::conditional_t<!std::is_void_v<RetT>, std::optional<RetT>, bool> {
    auto func = load<RetT, Args...>(name);
    if (!func.has_value()) {
      if constexpr (!std::is_void_v<RetT>)
        return std::nullopt;
      else
        return false;
    }

    if constexpr (!std::is_void_v<RetT>) {
      return std::invoke(func.value(), std::forward<Args>(args)...);
    } else {
      std::invoke(func.value(), std::forward<Args>(args)...);
      return true;
    }
  }

再根据上述代码分析,可以抽象出两个模型,conditionaltype以及conditionalinitial value。所以进行如下封装:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/// To replace void with VoidSubstituteT and none void with NoneVoidSubstituteT.
template <typename MayVoidT,
          typename VoidSubstituteT, typename NoneVoidSubstituteT = MayVoidT>
struct substitute_void {
  using type = std::conditional_t<std::is_void_v<MayVoidT>,
                                  VoidSubstituteT, NoneVoidSubstituteT>;
};
template <typename MayVoidT,
          typename VoidSubstituteT, typename NoneVoidSubstituteT = MayVoidT>
using substitute_void_t = typename substitute_void<MayVoidT,
                                                   VoidSubstituteT,
                                                   NoneVoidSubstituteT>::type;

/// Choose the initialization value for substitute_void_t.
template <typename MayVoidT,
          typename VoidSubstituteT, typename NoneVoidSubstituteT>
constexpr auto substitute_void_v(VoidSubstituteT&& t1,
                                 NoneVoidSubstituteT&& t2) {
  if constexpr (std::is_void_v<MayVoidT>) {
    return t1;
  } else {
    return t2;
  }
}

substitute_void_tsubstitute_void_v分别对应类型替换和初始值替换:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  template <typename RetT, typename... Args>
  auto invoke(CCString name, Args&&... args) const
  -> substitute_void_t<RetT, bool, std::optional<RetT>> {
    auto func = load<RetT, Args...>(name);
    if (!func.has_value()) {
      return substitute_void_v<RetT>(false, std::nullopt);
    }

    if constexpr (std::is_void_v<RetT>) {
      std::invoke(func.value(), std::forward<Args>(args)...);
      return true;
    } else {
      return std::invoke(func.value(), std::forward<Args>(args)...);
    }
  }

代码可读性以及圈复杂度都达到一个可以接受的标准。

结论

直观来看,constexpr if可以比较好的替代std::enable_if的SFINAE的编译期的重载方式,可以有效的减少重复代码,提高代码的可读性。多数情况下可以使用其来替代SFINAE,并写出更简洁的代码。

这个特性说明c++委员会在尝试推动“像写c++一样进行元编程”,降低元编程的复杂度。

参考资料

[1] conditional. https://en.cppreference.com/w/cpp/types/conditional

[2] if statement. https://en.cppreference.com/w/cpp/language/if