模板函数在类内和类外的定义有什么区别?

42

我想知道将模板函数声明为类外函数与在类内部声明相比是否有任何优势。

我正在努力清楚地理解这两种语法的优缺点。

以下是一个示例:

类外函数:

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
}

类中的Vs:

template<typename T>
struct MyType {
    template<typename... Args>
    void test(Args... args) const {
        // do things
    }
};

有没有语言特性在第一版或第二版中使用更容易?第一版会在使用默认模板参数或enable_if时变得阻碍吗?我想看到如何比较这两种情况与不同的语言特性,例如SFINAE以及可能的未来特性(模块?)。

考虑编译器特定的行为也可能很有趣。我认为MSVC在某些地方需要第一个代码片段中的inline,但我不确定。

编辑:我知道这些特征的工作方式没有区别,这主要是品味问题。我想看看这两种语法如何与不同的技术配合使用以及其中一种优势。我看到大多数答案都喜欢其中一种,但我真的想得到两者的看法。更客观的答案会更好。


1
这些实现具有不同的效果。后一个函数被隐式声明为“内联函数”,而前一个则没有。因此这是一个区别,而且如果你关心链接器的幸福,那么将函数声明为“内联函数”肯定是一个优点。 - IInspectable
我认为在功能方面没有任何区别,但是将定义放在另一个地方会使类的定义更加清晰,从而更容易找到函数。 - Rakete1111
2
@IInspectable 模板不受单一定义规则的限制,因此 inline 在这里没有作用。 - Quentin
你的意思是“定义”吗?声明模板函数在类外部和在类内部定义的优点。 - skypjack
是的,我是指定义。对于歧义表示抱歉。 - Guillaume Racicot
显示剩余2条评论
4个回答

22

就默认模板参数、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 中的定义。
表达能力
但是,除了增加可读性之外,内联和外部定义还有所不同。虽然每个内联定义都可以轻松地移动到外部定义中,但反之则不然。也就是说,外部定义比内联定义更具表现力。当您有紧密耦合的类相互依赖于彼此的功能,以至于前向声明不足时,就会发生这种情况。
例如,如果要支持命令模式的命令链并且使其支持用户定义的函数和函数对象而无需它们继承自某个基类,则出现此类情况。因此,这种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 和 Command 实现了调用运算符以及它们的大小,所以这里不足的前向声明是不够的。即使您通过指针存储成员命令,在定义 operator() 时也绝对需要 Command 的完整声明。
有了这个帮助器,我们可以实现 Command::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和默认模板参数在这两种变体中的实现方式。声明看起来完全相同,只有在定义中您必须删除任何默认参数。

  1. Default template parameters

    template <typename T = int>
    class A {
        template <typename U = void*>
        void someFunction(U val) {
            // do something
        }
    };
    

    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
    }
    
  2. enable_if in default template parameter

    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
        }
    };
    

    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
    }
    
  3. enable_if as non-type template parameter

    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
        }
    };
    

    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
    }
    

    Again, it is just missing the default parameter 0.

  4. SFINAE in return type

    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
        }
    };
    

    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
    }
    

    This time, since there are no default parameters, both declaration and definition actually look the same.


这实际上非常不错。每个点都解释得非常好。但是,它大多数情况下更偏向于一种语法而不是另一种。我对我的问题标题进行了编辑,希望现在更清晰了。 - Guillaume Racicot
@GuillaumeRacicot 它更喜欢一种语法而不是另一种,因为在类内定义中你无法做到的事情,在类外定义中都可以做到。类外定义唯一非常微小的劣势是编译器需要做更多的工作:它需要解析更多的标记,并确保类外定义与类中声明匹配。但这只有在整个项目中包含该文件数千次时才会注意到。即使有这个劣势,也将随着模块的出现而消失。 - Corristo
@GuillaumeRacicot 我添加了一些示例,展示了使用和不使用 std::enable_if 的 SFINAE 的不同用法以及如何传递默认参数。这是你想要看到的吗? - Corristo
谢谢,有了这个,它就更完整了。 - Guillaume Racicot
我会接受你的答案,但是你给我的主要原因关于完整类型和循环依赖是有缺陷的。看看你命令的这个实现,没有任何类外函数定义:http://coliru.stacked-crooked.com/a/d420ae7ba2ab9b08。甚至我可以反转定义:http://coliru.stacked-crooked.com/a/e79cfc7d69b6c8a2。 - Guillaume Racicot

19

第一版还是第二版中有更易使用的语言特性吗?

这是一个相当琐碎的情况,但值得一提的是:专门化。

举个例子,你可以通过外部定义来实现这个:

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>
}

如果你只想使用班级定义的函数做同样的事情,那么你需要为所有其他MyType函数复制代码(当然,假设test是你想专门化的唯一函数)。


作为一个示例:
template<>
struct MyType<int> {
    template<typename... Args>
    void test(Args...) const {
        // Specialized function
    }

    // Copy-and-paste of all the other functions...
};
当然,你仍然可以混合使用内联和外部定义来实现这一点,并且你拥有与完整的外部定义相同数量的代码。
不管怎样,我假设你是面向完全内联和完全外部定义的解决方案,因此混合使用是不可行的。
另一件你可以使用外部类定义而无法使用内联定义的事情是函数模板特化。
当然,你可以把主要定义放在内联中,但所有的特化都必须放在外部定义中。
在这种情况下,对于上述问题的答案是:即使有语言的功能你也不能同时使用其中一个版本
例如,考虑以下代码:
struct S {
    template<typename>
    void f();
};

template<>
void S::f<int>() {}

int main() {
    S s;
    s.f<int>();
}

假设类的设计者只想针对一些特定的类型提供f的实现,那么他就无法使用类内定义来实现这一点。


最后,独立定义可以帮助打破循环依赖关系。
这已经在大多数其他答案中提到了,例如这里这里,不必再举例说明。


1
该死!我真的很想接受多个答案。它们几乎都提出了很好的观点! - Guillaume Racicot
@GuillaumeRacicot 嗯,我试图让你了解到有些事情根本无法通过其中一种解决方案完成。其他所有情况都已经被其他答案探索过了,我没有再次提及它们。;-) - skypjack
@GuillaumeRacicot 增加了循环依赖关系的提醒,以便完整性。没有提供示例,因为在其他答案中已经有足够的示例了。;-) - skypjack
我对循环依赖的论点不太确定。看看我在另一个评论中留下的这个例子:http://coliru.stacked-crooked.com/a/d420ae7ba2ab9b08 或者你的循环依赖示例是关于其他事情的吗? - Guillaume Racicot
@GuillaumeRacicot 我明白你可以通过中间类或其他方式来解决这个问题。这也是我为什么添加了另一个回答,提供不同的动机和示例。我只是说离线定义有助于打破依赖性,并且并没有说它们是唯一的方法。 另一方面,特化类型根本无法放置在类内部。 不管怎样,这是一个非常有趣的好问题。 - skypjack

13

将声明与实现分离允许您做到这一点:

// 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();
    // ...
}
现在,这有什么用处呢?嗯,头文件baz.h现在可以包含bar.h并依赖于bar和其他声明,即使bar的实现依赖于baz.h
如果函数模板被定义为内联的,它必须在声明bar之前包含baz.h,如果baz.h依赖于bar,那么你就会有一个循环依赖关系。
此外,将函数(无论是模板还是非模板)定义为非内联的,将使声明以一种有效作为目录的形式保留下来,这比在定义满载的头文件中随处散布的声明更易于程序员阅读。当您使用提供结构化概述的专业编程工具时,这个优势会减弱。

1
我倾向于总是将它们合并 - 但如果它们是相互依存的,那么你就不能这样做。对于常规代码,通常将代码放在.cpp文件中,但对于模板来说,整个概念并不适用(并且会导致重复的函数原型)。例如:
template <typename T>
struct A {
    B<T>* b;
    void f() { b->Check<T>(); }
};

template <typename T>
struct B {
    A<T>* a;
    void g() { a->f(); }
};

当然,这只是一个虚构的例子,但可以将函数替换为其他内容。这两个类需要在使用之前定义。如果使用模板类的前向声明,则仍无法包含其中一个的函数实现。这是将它们放在外部的一个很好的理由,每次都可以100%解决问题。
另一种选择是将其中一个作为另一个的内部类。内部类可以超越其自身定义点访问外部类中的函数,因此问题有点隐藏,这在大多数情况下对于这些相互依赖的类是可用的。

“对于模板,这个概念并不是没有意义。” 是的。我看过很多定义模板函数在类外部的代码,也有很多在cpp文件中实现模板并进行显式实例化的代码。 - Guillaume Racicot
对于非模板代码,您大多需要在头文件中为用户提供类定义,并在某个可编译单元中提供实现,因此分离是有意义的。对于模板代码,两者都需要在头文件中,因此这个论点不成立。但这并不意味着人们不会这样做... - dascandy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接