部分模板专门化带来的麻烦

trouble with partial template specialisations

本文关键字:麻烦 专门化      更新时间:2023-10-16

我有以下的类结构

// file foo.h:
struct foo_base
{ ... }
template<typename T> struct foo : foo_base
{ ... };
template<typename F>
using is_foo = std::is_convertible<F,foo_base>;
template<typename, typename=void> struct aux;
template<typename Foo>
struct aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>
{ ... };           // specialisation for any foo

<一口>

// file bar.h:
#include "foo.h"
template<typename T> struct bar : foo<T>
{ ... };
template<typename T>
struct aux<bar<T>>
{ ... };           // specialisation for bar<T>

现在,问题是对于aux<bar<T>>,为aux提供的两种专门化都是可行的。有没有一种方法可以避免这种歧义而不为每个T提供另一种专门化?注意,对文件foo.h的修改不能知道文件bar.h

注意歧义必须解决,以便为任何aux<bar<T>>选择文件bar.h中提供的专门化。最初,bar并不是一个模板,而专门化aux<bar>也不是局部的,因此是首选。当将bar作为模板时,问题出现了。

由于第二个模板参数,编译器并不认为struct aux<bar<T>>struct aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>更专门化。您可以在bar<T>专门化中以相同的方式指定第二个参数:

template<typename T>
struct aux<bar<T>, typename std::enable_if<is_foo<bar<T>>::value>::type>
{ };

专门化部分模板的规则是复杂的,但我将尝试非常简单地解释:

三个(你的两个,加上我的一个)相关专业是

template<typename Foo>
struct aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>
template<typename T>
struct aux<bar<T>> // or aux<bar<T>, void>
{ };
template<typename T>
struct aux<bar<T>, typename std::enable_if<is_foo<bar<T>>::value>::type>
{ };

根据标准(14.5.5.2),要确定哪个类模板的部分特化是最特化的,需要回答的问题是以下哪个函数模板重载在调用f(aux<bar<T>>())时将是最佳匹配:

template<typename Foo>
void f(aux<Foo, typename std::enable_if<is_foo<Foo>::value>::type>); // 1
template<typename T>
void f(aux<bar<T>>); // or aux<bar<T>, void> // 2
template<typename T>
void f(aux<bar<T>, typename std::enable_if<is_foo<bar<T>>::value>::type>); // 3

反过来,函数的偏序规则说1不比2更专门化,2也不比1更专门化,粗略地说,因为1不明显比2更专门化,而2也不明显比1更专门化。"明显更专业化"并不是标准的说法,但这实际上意味着基于其中一个的类型参数,另一个的类型参数是不可演绎的。

当比较1和3时,1的参数可以由3演绎出来:Foo可以演绎为bar<T>。因此,3至少和1一样专门化。然而,3的论点不能从1推导出来:T根本不能推导出来。因此,编译器的结论是3比1更专门化。

类模板的局部专门化基于模式匹配。相比之下,自定义函数模板是基于模板实参演绎重载解析

由于问题中存在类层次结构,原则上通过函数模板重载可以更方便地定制行为,因为可以考虑派生到基的转换。在部分类模板专门化中使用的模式匹配不提供相同的灵活性。

但是,从c++ 11开始,可以进行编译时返回类型推断。下面是一个结合了标签调度、默认构造函数和decltype类型推导的解决方案:

#include <iostream>
// file foo_base.h:
struct foo_base 
{ 
    foo_base() = default; 
};
foo_base faux(foo_base const&)
{ 
    return foo_base{}; 
}
template<class T, class = decltype(faux(T{}))>
struct aux;
template<class T>
struct aux<T, foo_base> 
{ 
    enum { value = 1 }; 
};
// file foo.h:
template<typename T> 
struct foo : foo_base  
{ 
    foo() = default; 
};
// file bar.h:
template<typename T> 
struct bar : foo<T> 
{ 
    bar() = default; 
};
template<class T>
bar<T> faux(bar<T> const&) 
{ 
    return bar<T>{}; 
}
template<class T, class U>
struct aux<T, bar<U>> 
{ 
    enum { value = 2 }; 
};
// file meow.h
template<class T>
struct meow : bar<T>
{
    meow() = default;    
};
int main()
{
    std::cout << aux<foo_base>::value;  // 1
    std::cout << aux<foo<int>>::value;  // 1
    std::cout << aux<bar<int>>::value;  // 2
    std::cout << aux<meow<int>>::value; // 2
}

在c++ 11模式下(不需要c++ 14模式!)与g++和clang一起工作的实例

constexpr函数faux()foo_bar重载,并作为bar<T>的函数模板。从foo_base派生而不是从bar<T>派生的任何参数将选择前一个重载,从bar<T>派生的任何参数将选择后一个重载。该机制与标准库中的迭代器类别用于标签调度 std::advance()的几个实现相同,例如

要在类模板aux的部分专门化期间使用这种选择机制,还需要两个元素。首先,所有类都需要有一个默认构造函数。其次,将 decltype() 应用于表达式faux(T{}),以推导出返回类型

注意:不要求faux()constexpr,也不要求任何默认构造函数是constexpr,因为decltype()会不实际计算函数调用,而只是推断其返回值类型。

主类模板aux有一个默认的模板参数:

template<class T, class = decltype(faux(T{}))>
struct aux;

部分专门化foo_base的第二个参数允许您为任何从foo_base派生的类提供行为:

template<class T>
struct aux<T, foo_base> 
{ // custom behavior for anything derived from foo_bar };

第二个部分专门化匹配从任何模板实例化派生的任何类bar<U>

template<class T, class U>
struct aux<T, bar<U>> 
{ // custom behavior for anything derived from bar<U> for some U }

注意:主要的缺点是您可能需要为层次结构中的所有类提供默认构造函数。这可能是你能克服的障碍,也可能不是。大多数类都有默认构造函数,但有些类可能没有。从这个意义上说,这个解决方案是侵入性的(即,它不能被固定在现有代码的顶部,但它需要修改该代码)。