模板函数的越界定义 vs class

Out of line definition of template function vs in class

我想知道在 class 中声明模板函数是否有任何优势。

我想清楚地了解这两种语法的优缺点。

这是一个例子:

越线:

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 class:

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

第一版或第二版是否有更易于使用的语言功能?当使用默认模板参数或 enable_if 时,第一个版本会妨碍吗?我想看看这两个案例如何使用不同的语言功能(如 sfinae)以及可能的未来功能(模块?)进行比较。

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

编辑:我知道这些功能的工作方式没有区别,这主要是个人喜好问题。我想看看这两种语法如何使用不同的技术,以及一种语法相对于另一种语法的优势。我看到的大多数答案都偏向于另一个,但我真的很想得到双方。多回答objective就更好了。

将声明与实现分开允许您这样做:

// 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();
    // ...
}

现在,这有什么用?那么,header baz.h 现在可能包含 bar.h 并依赖于 bar 和其他声明,即使 bar 的实现依赖于 baz.h .

如果函数模板是内联定义的,它必须在声明 bar 之前包含 baz.h,并且如果 baz.h 依赖于 bar,那么您将有一个循环依赖。


除了解决循环依赖性之外,定义函数(无论是否为模板)out-of-line,将声明保留在一种有效地作为内容 table 工作的形式,这对程序员来说更容易阅读而不是散布在 header 满定义中的声明。当您使用专门的编程工具提供 header.

的结构化概览时,这种优势就会减弱。

关于默认模板参数、SFINAE 或 std::enable_if,这两个版本之间没有区别,因为重载解析和模板参数替换对它们的工作方式相同。我也没有看到任何理由为什么模块应该有所不同,因为它们不会改变编译器无论如何都需要查看成员函数的完整定义的事实。

可读性

外联版本的一个主要优点是可读性。您可以只声明和记录成员函数,甚至将定义移动到最后包含的单独文件中。这样一来,您的 class 模板的 reader 就不必跳过可能存在的大量实施细节,只需阅读摘要即可。

对于您的特定示例,您可以有定义

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 功能文档,大多数时候 class 的用户不需要查看 MyType_impl.h 中的定义.

表现力

但是,区分行外定义和行内定义的不仅仅是可读性的提高。class。虽然每个 in-class 定义都可以轻松移动到外联定义,但反之则不然。 IE。外联定义比 in-class 定义更具表现力。当您紧密耦合 class 依赖彼此功能的实体时会发生这种情况,因此前向声明是不够的。

一个这样的例子是命令模式,如果你希望它支持命令链 让它支持用户定义的函数和仿函数,而它们不必从某些基础 class 继承。所以这样的 Command 本质上是 std::function.

的 "improved" 版本

这意味着 Command class 需要某种形式的类型擦除,我将在此处省略,但如果有人真的希望我包含它,我可以添加它。

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?最简单的方法是有一个助手 class 存储原始 CommandCommand 之后执行,然后按顺序调用它们的两个调用运算符:

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 的 class 声明必须在 CommandNextHelper 的声明之前,这意味着您根本无法在-[=123= 中定义 .then 函数].它的定义必须在 CommandNextHelper.

声明之后

我知道这不是一个简单的示例,但我想不出一个更简单的示例,因为当您绝对必须将某个运算符定义为 class 成员时,这个问题通常会出现。这主要适用于表达式模板中的 operator()operator[],因为这些运算符不能定义为非成员。

结论

因此得出结论:您更喜欢哪一种主要是品味问题,因为两者之间没有太大区别。仅当 classes 之间存在循环依赖时,才不能对所有成员函数使用 in-class 定义。无论如何,我个人更喜欢行外定义,因为外包函数声明的技巧也可以帮助文档生成工具,例如 doxygen,它只会为实际的 class 创建文档,而不是为额外的助手创建文档在另一个文件中定义和声明。


编辑

如果我正确理解了您对原始问题的编辑,您希望了解两种变体的 SFINAE、std::enable_if 和默认模板参数的一般情况。声明看起来完全一样,只是对于定义,如果有的话你必须删除默认参数。

  1. 默认模板参数

    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
    }
    
  2. 默认模板参数
  3. 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
    }
    
  4. 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。

  5. SFINAE return 类型

    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
    }
    

    这一次,因为没有默认参数,所以声明和定义实际上看起来是一样的。

我倾向于总是合并它们——但如果它们是相互依赖的,你就不能这样做。对于常规代码,您通常将代码放在 .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(); }
};

当然这是一个人为的例子,但用其他东西替换了函数。这两个 class 需要在使用前相互定义。如果您使用模板的前向声明 class,您仍然无法包含其中之一的函数实现。这是让它们脱节的一个很好的理由,每次都会 100% 解决这个问题。

一种替代方法是使其中一个成为另一个的内部 class。内部 class 可以延伸到外部 class 超出其自身的函数定义点,因此问题有点隐藏,当您具有这些相互依赖的 classes 时,这在大多数情况下是可用的.

Are there language features that are easier to use with the first or second version?

一个非常微不足道的案例,但值得一提:specializations.

例如,您可以使用外联定义来执行此操作:

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

如果你只想对 in-class 定义做同样的事情,你必须复制 MyType 的所有其他函数的代码(假设 test 是唯一的当然,你想要专攻的功能)。
例如:

template<>
struct MyType<int> {
    template<typename... Args>
    void test(Args...) const {
        // Specialized function
    }

    // Copy-and-paste of all the other functions...
};

当然,您仍然可以混合 in-class 和外联定义来做到这一点,并且您拥有与完整外联版本相同数量的代码。
不管怎样,我假设你是面向全内-class 和全外线解决方案,因此混合的是不可行的。


另一件你可以用外联 class 定义做而你根本不能用 in-class 定义做的事情是函数模板特化。
当然,你可以把primary definition放在-class,但是所有的specializations都必须放在out-of-line里。

在这种情况下,上述问题的答案是:甚至存在您无法使用其中一个版本的语言的功能

例如,考虑以下代码:

struct S {
    template<typename>
    void f();
};

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

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

假设 class 的设计者想要为 f 提供仅针对少数特定类型的实现。
他根本无法使用 in-class 定义来做到这一点。


最后,外联定义有助于打破循环依赖。
这个在 中已经提到了,不值得再举一个例子。