首頁>技術>

一個典型的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做特化版本?原因主要有兩點:

做不同的特化版本會導致函式體內的實現出現冗餘,在if判斷前後會有大量相同的程式碼存在於不同的特化版本中,導致維護成本高;模板偏特化是針對具體型別的特化,而在此處,我們並不在意模板引數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的詳細概念要展開來討論得再開個系列了,我也是剛開始接觸,理解不到位的地方,請大家斧正。

簡單來講,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的寶藏剛剛開啟,還有非常多值得挖掘的地方。

參考文獻https://omnigoat.github.io/2020/01/19/cpp20-concepts/https://en.cppreference.com/w/cpp/language/constraints

13
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 每次10分鐘跟我學Python(第三十五次課)