使用 SFINAE 进行模板类专业化

2021-12-13 00:00:00 templates c++ c++11 sfinae

假设我有这些声明

template<typename T> class User;
template<typename T> class Data;

并且想要为T = Data以及从DataUser<>> 但也允许在别处定义的其他专业化.

and want to implement User<> for T = Data<some_type> and any class derived from Data<some_type> but also allow for other specialisations defined elsewhere.

如果我还没有类模板User<>的声明,我可以简单地

If I didn't already have the declaration of the class template User<>, I could simply

template<typename T,
         typename A= typename std::enable_if<is_Data<T>::value>::type>
class User { /*...*/ };

哪里

template<template<typename> data>> struct is_Data
{ static const bool value = /* some magic here (not the question) */; };

然而,这有两个模板参数,因此与之前的声明冲突,其中 User<> 仅使用一个模板参数声明.还有什么我可以做的吗?

However, this has two template parameters and thus clashes with the previous declaration, where User<> is declared with only one template parameter. Is there anything else I can do?

(注意

template<typename T,
         typename A= typename std::enable_if<is_Data<T>::value>::type>
class User<T> { /*...*/ };

不起作用(在部分特化中不能使用默认模板参数),也没有

template<typename T> class User<Data<T>> { /*...*/ };

因为它不允许从 Data<> 派生的类型,所以也不允许

as it doesn't allow types derived from Data<>, neither does

template<typename T>
class User<typename std::enable_if<is_Data<T>::value,T>::type>
{ /*...*/ };

因为模板参数T未用于部分特化.)

since template parameter T is not used in partial specialization.)

推荐答案

既然你说你还在等待更好的答案,这就是我的看法.它并不完美,但我认为它可以让您尽可能使用 SFINAE 和部分专业化.(我猜 Concepts 会提供一个完整而优雅的解决方案,但我们必须等待更长的时间.)

Since you said you were still waiting for a better answer, here's my take on it. It's not perfect, but I think it gets you as far as possible using SFINAE and partial specializations. (I guess Concepts will provide a complete and elegant solution, but we'll have to wait a bit longer for that.)

该解决方案依赖于最近才在 C++14 最终版本之后的标准工作草案中指定的别名模板功能,但已被实现支持一段时间.N4527草案[14.5.7p3]中的相关措辞是:

The solution relies on a feature of alias templates that was specified only recently, in the standard working drafts after the final version of C++14, but has been supported by implementations for a while. The relevant wording in draft N4527 [14.5.7p3] is:

然而,如果模板 id 是相关的,后续的模板参数替换仍然适用于模板 id.[示例:

However, if the template-id is dependent, subsequent template argument substitution still applies to the template-id. [ Example:

template<typename...> using void_t = void;
template<typename T> void_t<typename T::foo> f();
f<int>(); // error, int does not have a nested type foo

――结束示例 ]

这是一个实现这个想法的完整示例:

Here's a complete example implementing this idea:

#include <iostream>
#include <type_traits>
#include <utility>

template<typename> struct User { static void f() { std::cout << "primary
"; } };

template<typename> struct Data { };
template<typename T, typename U> struct Derived1 : Data<T*> { };
template<typename> struct Derived2 : Data<double> { };
struct DD : Data<int> { };

template<typename T> void take_data(Data<T>&&);

template<typename T, typename = decltype(take_data(std::declval<T>()))> 
using enable_if_data = T;

template<template<typename...> class TT, typename... Ts> 
struct User<enable_if_data<TT<Ts...>>> 
{ 
    static void f() { std::cout << "partial specialization for Data
"; } 
};

template<typename> struct Other { };
template<typename T> struct User<Other<T>> 
{ 
    static void f() { std::cout << "partial specialization for Other
"; } 
};

int main()
{
    User<int>::f();
    User<Data<int>>::f();
    User<Derived1<int, long>>::f();
    User<Derived2<char>>::f();
    User<DD>::f();
    User<Other<int>>::f();
}

运行它打印:

primary
partial specialization for Data
partial specialization for Data
partial specialization for Data
primary
partial specialization for Other

正如你所看到的,有一个问题:DD 没有选择部分特化,而且它不能,因为我们声明它的方式.那么,我们为什么不直接说

As you can see, there's a wrinkle: the partial specialization isn't selected for DD, and it can't be, because of the way we declared it. So, why don't we just say

template<typename T> struct User<enable_if_data<T>> 

并允许它匹配DD?这实际上在 GCC 中有效,但被 Clang 和 MSVC 正确拒绝,因为 [14.5.5p8.3, 8.4]([p8.3] 将来可能会消失,因为它是多余的 - CWG 2033):

and allow it to match DD as well? This actually works in GCC, but is correctly rejected by Clang and MSVC because of [14.5.5p8.3, 8.4] ([p8.3] may disappear in the future, as it's redundant - CWG 2033):

  • 专业化的参数列表不应与主模板的隐式参数列表.
  • 专业化应比主模板 (14.5.5.2) 更专业化.

User<enable_if_data<T>> 等价于 User(对默认参数的模替换,单独处理,如第上面引用),因此是部分专业化的无效形式.不幸的是,匹配诸如 DD 之类的东西通常需要 T 形式的部分特化参数 - 没有其他形式它可以拥有并且仍然匹配每种情况.所以,恐怕我们可以肯定地说,这部分无法在给定的约束内解决.(有 核心问题 1980,其中暗示关于模板别名使用的一些可能的未来规则,但我怀疑它们会使我们的案例有效.)

User<enable_if_data<T>> is equivalent to User<T> (modulo substitution into that default argument, which is handled separately, as explained by the first quote above), thus an invalid form of partial specialization. Unfortunately, matching things like DD would require, in general, a partial specialization argument of the form T - there's no other form it can have and still match every case. So, I'm afraid we can conclusively say that this part cannot be solved within the given constraints. (There's Core issue 1980, which hints at some possible future rules regarding the use of template aliases, but I doubt they'll make our case valid.)

只要从 Data<T> 派生的类本身是模板特化,使用上述技术进一步限制它们将起作用,所以希望这对您有用.

As long as the classes derived from Data<T> are themselves template specializations, further constraining them using the technique above will work, so hopefully this will be of some use to you.

编译器支持(这是我测试过的,其他版本也可以):

Compiler support (this is what I tested, other versions may work as well):

  • Clang 3.3 - 3.6.0,带有 -Wall -Wextra -std=c++11 -pedantic - 如上所述.
  • GCC 4.7.3 - 4.9.2,相同的选项 - 同上.奇怪的是,GCC 5.1.0 - 5.2.0 不再使用正确版本的代码选择部分特化.这看起来像是一种回归.我没有时间整理一份正确的错误报告;如果你愿意,可以随意去做.该问题似乎与使用参数包和模板模板参数有关.无论如何,GCC 使用 enable_if_data 接受不正确的版本,因此这可以作为临时解决方案.
  • MSVC:带有 /W4 的 Visual C++ 2015,按上述方式工作.旧版本不喜欢默认参数中的 decltype,但该技术本身仍然有效 - 用另一种表达约束的方式替换默认参数使其适用于 2013 Update 4.
  • Clang 3.3 - 3.6.0, with -Wall -Wextra -std=c++11 -pedantic - works as described above.
  • GCC 4.7.3 - 4.9.2, same options - same as above. Curiously, GCC 5.1.0 - 5.2.0 no longer selects the partial specialization using the correct version of the code. This looks like a regression. I don't have time to put together a proper bug report; feel free to do it if you want. The problem seems to be related to the use of parameter packs together with a template template parameter. Anyway, GCC accepts the incorrect version using enable_if_data<T>, so that can be a temporary solution.
  • MSVC: Visual C++ 2015, with /W4, works as described above. Older versions don't like the decltype in the default argument, but the technique itself still works - replacing the default argument with another way of expressing the constraint makes it work on 2013 Update 4.

相关文章