Gui*_*cot 30 c++ templates code-readability
我想知道在类中声明模板功能是否有任何优势.
我试图清楚地了解这两种语法的优缺点.
这是一个例子:
不合时宜:
template<typename T>
struct MyType {
template<typename... Args>
void test(Args...) const;
};
template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
// do things
}
Run Code Online (Sandbox Code Playgroud)
同班同学:
template<typename T>
struct MyType {
template<typename... Args>
void test(Args... args) const {
// do things
}
};
Run Code Online (Sandbox Code Playgroud)
是否有第一版或第二版更容易使用的语言功能?使用默认模板参数或enable_if时,第一个版本是否会妨碍?我想看看这两个案例如何使用不同的语言特征(如sfinae)以及未来潜在的特征(模块?)进行比较.
将编译器特定行为考虑在内也很有趣.我认为MSVC需要inline在某些地方使用第一个代码片段,但我不确定.
编辑:我知道这些功能的工作方式没有区别,这主要是品味问题.我想看看两种语法如何使用不同的技术,以及一种优于另一种的优势.我看到大多数答案都有利于一个人,但我真的很想得到双方的支持.更客观的答案会更好.
eer*_*ika 12
将声明与实现分离可以实现以下目的:
// file bar.h
// headers required by declaration
#include "foo.h"
// template declaration
template<class T> void bar(foo);
// headers required by the definition
#include "baz.h"
// template definition
template<class T> void bar(foo) {
baz();
// ...
}
Run Code Online (Sandbox Code Playgroud)
现在,是什么让这有用?好吧,标题baz.h现在可以包括bar.h并依赖于bar和其他声明,即使实现bar取决于baz.h.
如果函数模板是内联定义的,则必须baz.h在声明之前包含bar,如果baz.h取决于bar,则您将具有循环依赖性.
除了解决循环依赖之外,定义函数(无论是否为模板),将声明留在一个有效地作为内容表的形式中,这使得程序员更容易阅读,而不是在充满定义的标题中散布的声明.当您使用提供标题结构化概述的专用编程工具时,此优势会减少.
Cor*_*sto 12
两个版本之间在默认模板参数,SFINAE或std::enable_if重载解析方面没有区别,模板参数的替换对它们两者的工作方式相同.我也没有看到为什么应该与模块存在差异的任何原因,因为它们不会改变编译器需要查看成员函数的完整定义的事实.
外部版本的一个主要优点是可读性.您只需声明并记录成员函数,甚至可以将定义移动到最后包含的单独文件中.这使得类模板的读者不必跳过可能大量的实现细节,只需阅读摘要即可.
对于您的特定示例,您可以拥有定义
template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
// do things
}
Run Code Online (Sandbox Code Playgroud)
在一个调用的文件中MyType_impl.h,然后让文件MyType.h只包含声明
template<typename T>
struct MyType {
template<typename... Args>
void test(Args...) const;
};
#include "MyType_impl.h"
Run Code Online (Sandbox Code Playgroud)
如果MyType.h包含足够的函数文档,那么MyType大多数时候该类的用户不需要查看其中的定义MyType_impl.h.
但是,增加可读性不仅可以区分外线和类内定义.虽然每个类内定义都可以轻松地移动到一个外联定义,但反过来却并非如此.即外线定义更具有内在定义的表现力.当你有紧密耦合的类依赖于彼此的功能以便前向声明不够时,就会发生这种情况.
一个这样的情况是例如命令模式,如果你希望它支持命令的链接,并且它支持用户定义的函数和函子,而不必从一些基类继承.所以这样一个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;
};
Run Code Online (Sandbox Code Playgroud)
那你将如何实施.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;
};
Run Code Online (Sandbox Code Playgroud)
请注意,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);
}
Run Code Online (Sandbox Code Playgroud)
再次注意,如果CommandNextHelper只是向前声明,这不起作用,因为编译器需要知道构造函数的声明CommandNextHelper.既然我们已经知道类声明Command必须在声明之前来CommandNextHelper,这意味着你根本无法.then在类中定义函数.它的定义必须在声明之后CommandNextHelper.
我知道这不是一个简单的例子,但我想不出更简单的例子,因为当你绝对必须将一些运算符定义为类成员时,这个问题大多会出现.这主要适用于operator()和operator[]在模板中,因为这些运算符不能被定义为非成员.
因此得出结论:这主要取决于您喜欢哪种口味,因为两者之间没有太大区别.只有在类之间存在循环依赖关系时,才能对所有成员函数使用类内定义.我个人更喜欢外部定义,因为外包函数声明的技巧也可以帮助文档生成工具,如doxygen,这将只为实际类创建文档,而不是为定义和声明的其他帮助程序在另一个文件中.
如果我理解您对原始问题的正确编辑,您希望std::enable_if了解两种变体的SFINAE 和默认模板参数的一般情况.声明看起来完全相同,仅适用于必须删除默认参数的定义.
默认模板参数
template <typename T = int>
class A {
template <typename U = void*>
void someFunction(U val) {
// do something
}
};
Run Code Online (Sandbox Code Playgroud)
VS
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
}
Run Code Online (Sandbox Code Playgroud)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
}
};
Run Code Online (Sandbox Code Playgroud)
VS
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
}
Run Code Online (Sandbox Code Playgroud)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
}
};
Run Code Online (Sandbox Code Playgroud)
VS
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
}
Run Code Online (Sandbox Code Playgroud)
同样,它只是缺少默认参数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
}
};
Run Code Online (Sandbox Code Playgroud)
VS
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
}
Run Code Online (Sandbox Code Playgroud)
这次,由于没有默认参数,声明和定义实际上看起来都是一样的.
是否有第一版或第二版更容易使用的语言功能?
一个相当微不足道的案例,但值得一提:专业化.
例如,您可以使用外线定义执行此操作:
template<typename T>
struct MyType {
template<typename... Args>
void test(Args...) const;
// Some other functions...
};
template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
// do things
}
// Out-of-line definition for all the other functions...
template<>
template<typename... Args>
void MyType<int>::test(Args... args) const {
// do slightly different things in test
// and in test only for MyType<int>
}
Run Code Online (Sandbox Code Playgroud)
如果你只想对类内定义做同样的事情,你必须复制所有其他函数的代码MyType(假设test是你想要专门化的唯一函数,当然).
举个例子:
template<>
struct MyType<int> {
template<typename... Args>
void test(Args...) const {
// Specialized function
}
// Copy-and-paste of all the other functions...
};
Run Code Online (Sandbox Code Playgroud)
当然,您仍然可以混合使用内部和外部定义来执行此操作,并且您拥有与完整外部版本相同数量的代码.
无论如何,我认为你的目标是完整的课堂和完整的外部解决方案,因此混合的解决方案是不可行的.
您可以使用外部类定义执行另一项操作,而根本不能使用类内定义,这是函数模板特化.
当然,您可以将主要定义放在类中,但所有专业化必须放在不合适的位置.
在这种情况下,上述问题的答案是:甚至存在您不能与其中一个版本一起使用的语言功能.
例如,请考虑以下代码:
struct S {
template<typename>
void f();
};
template<>
void S::f<int>() {}
int main() {
S s;
s.f<int>();
}
Run Code Online (Sandbox Code Playgroud)
假设该类的设计者想要f仅为少数特定类型提供实现.
他根本无法用类内定义来做到这一点.
最后,外线定义有助于打破循环依赖关系.
在大多数 其他答案中已经提到过这一点,并且给出另一个例子并不值得.
| 归档时间: |
|
| 查看次数: |
5041 次 |
| 最近记录: |