06a236816bdd865135a2f352abaa4fe2.png

一个典型的stackoverflow风格的问题作为文章标题,以此避免挖空心思想名字的纠结。

问题描述

回到这个问题,常写模板的同学应该能心领神会需求的来源。

举个简单例子,在三维数据处理中,经常会和空间点point打交道,point中会有很多种信息需要存储,比如空间坐标 x/y/z,比如法向信息 nx/ny/nz,比如颜色信息 R/G/B等,这些信息并非都是必须,在不同应用中会有不同的取舍。为了简单起见,定义两个模板类 point_xyz, point_xyz_normal。两者的差异是带法向的模板类多了取值函数 nx(),ny(),nz()。

template<typename value_type>
class point_xyz
{
public:
       const value_type& x()const { return xyz_[0]; }
       const value_type& y()const { return xyz_[1]; }
       const value_type& z()const { return xyz_[2]; }
protected:
	Eigen::Matrix<value_type, 3, 1> xyz_;
};

template <typename FloatType>
class point_xyz_normal
{
public:
       const value_type& x()const { return xyz_[0]; }
       const value_type& y()const { return xyz_[1]; }
       const value_type& z()const { return xyz_[2]; }
	
       const value_type& nx()const { return nxyz_[0]; }
       const value_type& ny()const { return nxyz_[1]; }
       const value_type& nz()const { return nxyz_[2]; }
			
protected:
	Eigen::Matrix<value_type, 3, 1> xyz_;
	Eigen::Matrix<value_type, 3, 1> nxyz_;
};

接下来我们定义另一个处理函数func,其定义如下:

template <typename PointAttribute>
void func(const PointAttribute &p)
{
   if p has nx():
    ......
   if p doesn't have nx():
    ......
}

当然上述代码无法通过编译,写在这只是为了表示,func函数中希望针对p的不同性质做不同的处理。

有同学可能会问,为什么不用模板偏特化,针对point_xyz,和point_xyz_normal做特化版本?原因主要有两点:

  1. 做不同的特化版本会导致函数体内的实现出现冗余,在if判断前后会有大量相同的代码存在于不同的特化版本中,导致维护成本高;
  2. 模板偏特化是针对具体类型的特化,而在此处,我们并不在意模板参数PointAttribute是什么类型,只需要判断PointAttribute中是否存在函数nx。

解决方法

怎么办呢?找遍网络,发现得依靠C++20的Concepts和Requires来实现。先上答案:

template <typename PointAttribute>
concept has_normal = requires(PointAttribute t)
{
   t.nx();
   t.ny();
   t.nz(); 
};

template <typename PointAttribute>
void func(const PointAttribute &p)
{
   if constexpr (has_normal<PointCloud::point_attribute>){
      ...
   }else{
      ...
   }
}

以上代码需要编译器支持C++20特性才能编译。msvc 2019并不支持完整的C++20特性,而且需要开启/std:c++latest才能使用。如果你使用的是CMake,需要用下列语句来启用,否则msvc默认的__cplusplus版本号还停留在1997呢。

target_compile_options(${target_name} PRIVATE "/Zc:__cplusplus")
target_compile_options(${target_name} PRIVATE "/std:c++latest")

Concept和Requires的详细概念要展开来讨论得再开个系列了,我也是刚开始接触,理解不到位的地方,请大家斧正。

解释与讨论

Omni Blogs 做了个非常好的介绍。我借用其中的例子来做讨论。

简单来讲,Concept和SFINAE的行为有点像,都可以对模板做出一定的约束。我们看下例,希望对不同类型的模板参数T调用不同的log处理函数。注意,此处无法针对具体类型做偏特化,因为要判断T是整形或浮点型,而不是判断T是int还是double,需要注意其中的差别。

template <typename T>
void log(T&& x)
{
    log_integral(x);
}

template <typename T>
void log(T&& x)
{
    log_floating_point(x);
}

如果使用SFINAE,其实现如下:

template <typename T, typename = std::enable_if_t<std::is_integral_v<T>>>
void log(T&& x)
{ /* implementation irrelevant */ }

template <typename T, typename = std::enable_if_t<std::is_floating_point_v<T>>>
void log(T&& x)
{ /* implementation irrelevant */ }

或者可以借用constexpr让代码变短一点:

template <typename T>>
void log(T&& x)
{ 
   if constexpr (std::is_integral_v<T>)
   {   
   }else if constexpr (std::is_floating_point_v<T>)
   {
   }
}

而使用Concept可以让代码含义更清晰:

template <typename T>
requires std::integral<T>
void log(T&& x)
{ ... }

template <typename T>
requires std::floating_point<T>
void log(T&& x)
{ ... }

其中requires表示模板参数T需要满足特定的条件,而这里的条件分别是std::integral<T>和std::floating_point<T>。

Concept的写法有许多种,在此不赘述,感兴趣的可以看Omni Blog。回到本文的问题,我们定义了一个concept,要求t中具备nx等函数,而这个concept可以用 if constexpr在编译期判断。

template <typename PointAttribute>
concept has_normal = requires(PointAttribute t)
{
   t.nx();
   t.ny();
   t.nz();
};

至此,问题解决,但concept的宝藏刚刚打开,还有非常多值得挖掘的地方。

参考文献

  1. omni blogs
  2. https://en.cppreference.com/w/cpp/language/constraints
Logo

前往低代码交流专区

更多推荐