关于默认模板参数,SFINAE 或std::enable_if 的两个版本之间没有区别,因为重载决议和模板参数的替换对它们的工作方式相同。我也看不出模块应该有区别的任何理由,因为它们不会改变编译器无论如何都需要查看成员函数的完整定义的事实。
可读性
离线版本的一个主要优点是可读性。您可以只声明和记录成员函数,甚至可以将定义移动到最后包含的单独文件中。这样一来,您的类模板的读者就不必跳过可能大量的实现细节,而只需阅读摘要即可。
对于您的特定示例,您可以有定义
template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
// do things
}
在名为 MyType_impl.h 的文件中,然后让文件 MyType.h 仅包含声明
template<typename T>
struct MyType {
template<typename... Args>
void test(Args...) const;
};
#include "MyType_impl.h"
如果MyType.h 包含足够的MyType 函数文档,则大多数时候该类的用户不需要查看MyType_impl.h 中的定义。
表现力
但是,区分行外定义和类内定义的不仅仅是增加了可读性。虽然每个课堂定义都可以很容易地移到线外定义,但反之则不然。 IE。线外定义比类内定义更具表现力。当您有紧密耦合的类相互依赖于彼此的功能时,就会发生这种情况,因此前向声明是不够的。
一个这样的情况是例如。如果您希望命令模式支持命令链接并且让它支持用户定义的函数和仿函数,而不必从某些基类继承。所以这样的Command本质上是std::function的“改进”版本。
这意味着Command 类需要某种形式的类型擦除,我将在此省略,但如果有人真的希望我包含它,我可以添加它。
template <typename T, typename R> // T is the input type, R is the return type
class Command {
public:
template <typename U>
Command(U const&); // type erasing constructor, SFINAE omitted here
Command(Command<T, R> const&) // copy constructor that makes a deep copy of the unique_ptr
template <typename U>
Command<T, U> then(Command<R, U> next); // chaining two commands
R operator()(T const&); // function call operator to execute command
private:
class concept_t; // abstract type erasure class, omitted
template <typename U>
class model_t : public concept_t; // concrete type erasure class for type U, omitted
std::unique_ptr<concept_t> _impl;
};
那么你将如何实现.then?最简单的方法是创建一个辅助类来存储原始的 Command 和 Command 以在之后执行,然后按顺序调用它们的两个调用运算符:
template <typename T, typename R, typename U>
class CommandThenHelper {
public:
CommandThenHelper(Command<T,R>, Command<R,U>);
U operator() (T const& val) {
return _snd(_fst(val));
}
private:
Command<T, R> _fst;
Command<R, U> _snd;
};
请注意,在此定义处 Command 不能是不完整类型,因为编译器需要知道 Command<T,R> 和 Command<R, U> 实现了调用运算符以及它们的大小,因此前向声明在这里是不够的.即使您要通过指针存储成员命令,对于operator() 的定义,您也绝对需要Command 的完整声明。
有了这个助手,我们可以实现Command<T,R>::then:
template <typename T, R>
template <typename U>
Command<T, U> Command<T,R>::then(Command<R, U> next) {
// this will implicitly invoke the type erasure constructor of Command<T, U>
return CommandNextHelper<T, R, U>(*this, next);
}
再次注意,如果 CommandNextHelper 仅被前向声明,这将不起作用,因为编译器需要知道 CommandNextHelper 的构造函数的声明。由于我们已经知道Command 的类声明必须在CommandNextHelper 的声明之前,这意味着您根本无法在类中定义.then 函数。它的定义必须在CommandNextHelper的声明之后。
我知道这不是一个简单的例子,但我想不出一个更简单的例子,因为当你绝对必须将某个运算符定义为类成员时,通常会出现这个问题。这主要适用于表达式模板中的operator() 和operator[],因为这些运算符不能定义为非成员。
结论
因此得出结论:您更喜欢哪一种主要取决于口味,因为两者之间没有太大区别。仅当您在类之间具有循环依赖关系时,您才能对所有成员函数使用类内定义。无论如何,我个人更喜欢离线定义,因为外包函数声明的技巧还可以帮助文档生成工具,例如 doxygen,然后它只会为实际类创建文档,而不是为定义和声明的其他帮助程序创建文档在另一个文件中。
编辑
如果我正确理解您对原始问题的编辑,您希望了解 SFINAE、std::enable_if 和默认模板参数对于这两个变体的一般情况。声明看起来完全一样,只是对于必须删除默认参数的定义(如果有的话)。
-
默认模板参数
template <typename T = int>
class A {
template <typename U = void*>
void someFunction(U val) {
// do something
}
};
对
template <typename T = int>
class A {
template <typename U = void*>
void someFunction(U val);
};
template <typename T>
template <typename U>
void A<T>::someFunction(U val) {
// do something
}
-
默认模板参数中的enable_if
template <typename T>
class A {
template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
bool someFunction(U const& val) {
// do some stuff here
}
};
对比
template <typename T>
class A {
template <typename U, typename = std::enable_if_t<std::is_convertible<U, T>::value>>
bool someFunction(U const& val);
};
template <typename T>
template <typename U, typename> // note the missing default here
bool A<T>::someFunction(U const& val) {
// do some stuff here
}
-
enable_if 作为非类型模板参数
template <typename T>
class A {
template <typename U, std::enable_if_t<std::is_convertible<U, T>::value, int> = 0>
bool someFunction(U const& val) {
// do some stuff here
}
};
对比
template <typename T>
class A {
template <typename U, std::enable_if_t<std::is_convertible<U, T>::value, int> = 0>
bool someFunction(U const& val);
};
template <typename T>
template <typename U, std::enable_if_t<std::is_convertible<U, T>::value, int>>
bool A<T>::someFunction(U const& val) {
// do some stuff here
}
同样,它只是缺少默认参数 0。
-
返回类型中的 SFINAE
template <typename T>
class A {
template <typename U>
decltype(foo(std::declval<U>())) someFunction(U val) {
// do something
}
template <typename U>
decltype(bar(std::declval<U>())) someFunction(U val) {
// do something else
}
};
对
template <typename T>
class A {
template <typename U>
decltype(foo(std::declval<U>())) someFunction(U val);
template <typename U>
decltype(bar(std::declval<U>())) someFunction(U val);
};
template <typename T>
template <typename U>
decltype(foo(std::declval<U>())) A<T>::someFunction(U val) {
// do something
}
template <typename T>
template <typename U>
decltype(bar(std::declval<U>())) A<T>::someFunction(U val) {
// do something else
}
这一次,由于没有默认参数,所以声明和定义实际上看起来是一样的。