【问题标题】:Out of line definition of template function vs in class模板函数与类中的离线定义
【发布时间】:2016-11-02 13:52:58
【问题描述】:

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

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

这是一个例子:

越界:

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
}

在课堂上的对比:

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

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

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

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

【问题讨论】:

  • 这些实现有不同的效果。后一个函数被隐式声明为inline,前者不是。无论如何,这会有所不同,如果您关心一个快乐的链接器,那么声明函数 inline 肯定是一个优势。
  • 我不认为在功能上有什么区别,但是不合常规的把定义放在了别的地方,清理了类定义,所以查找函数变得更容易了。
  • @IInspectable 模板不受单一定义规则的约束,因此inline 在这里无效。
  • 我发现当函数定义不正确时,阅读MyType 所做的总结会更容易。它更难写,但我优先考虑可读性而不是可写性:howardhinnant.github.io/coding_guidelines.html
  • 在类中声明模板函数的优势 定义吗?

标签: c++ templates code-readability


【解决方案1】:

关于默认模板参数,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?最简单的方法是创建一个辅助类来存储原始的 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&lt;T,R&gt;Command&lt;R, U&gt; 实现了调用运算符以及它们的大小,因此前向声明在这里是不够的.即使您要通过指针存储成员命令,对于operator() 的定义,您也绝对需要Command 的完整声明。

有了这个助手,我们可以实现Command&lt;T,R&gt;::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. 默认模板参数

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

  4. 返回类型中的 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
    }
    

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

【讨论】:

  • 这个其实还不错。每一点都解释得很好。但同样,它最喜欢一种语法而不是另一种。我对问题的标题进行了编辑,希望现在更清楚了。
  • @GuillaumeRacicot 它偏爱一种语法而不是另一种语法,因为您无法使用类内定义做任何事情,而您无法使用外部定义做任何事情。外联定义的唯一非常小的缺点是编译器必须做更多的工作:它需要解析更多的标记,并且必须确保外联定义与类中的声明相匹配。但这不应该引起注意,除非您在项目中包含数千次文件。甚至这个缺点也会随着模块而消失。
  • @GuillaumeRacicot 我添加了一些示例,展示了使用和不使用 std::enable_if 的 SFINAE 的外观以及如何传递默认参数。这是你想看到的吗?
  • 谢谢,这样就更完整了。
  • 我会接受你的回答,但你给我的主要原因是关于完整类型和循环依赖存在缺陷。查看您的命令的这个实现,没有任何类外函数定义:coliru.stacked-crooked.com/a/d420ae7ba2ab9b08。见鬼,我什至可以颠倒定义:coliru.stacked-crooked.com/a/e79cfc7d69b6c8a2
【解决方案2】:

第一版或第二版是否有更易于使用的语言功能?

一个相当琐碎的案例,但值得一提的是:专业化

例如,您可以使用行外定义来做到这一点:

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 提供一个仅针对少数特定类型的实现。
他根本无法通过课堂定义来做到这一点。


最后,行外定义有助于打破循环依赖。
这在most ofthe other answers已经提到过,不值得再举一个例子。

【讨论】:

  • 该死!我非常想接受多个答案。他们几乎都带来了好点!
  • @GuillaumeRacicot 好吧,我试图让您了解一个事实,即使用其中一种解决方案根本无法完成某些事情。所有其他案例都已经被其他答案探索过了,我没有第二次提到它们。 ;-)
  • @GuillaumeRacicot 添加了关于完整性的循环依赖项的提及。在其他答案中,您已经没有足够的示例了。 ;-)
  • 我不太确定循环依赖参数。看看我在另一条评论中留下的这个例子:coliru.stacked-crooked.com/a/d420ae7ba2ab9b08 或者你的循环依赖示例可能是关于其他的?
  • @GuillaumeRacicot 我知道您可以通过中间类或者其他方式来解决它。这也是为什么我添加了一个具有不同动机和示例的答案。我只是说离线定义有助于打破依赖关系,而不是它们是这样做的唯一方法。另一方面,专业化根本不能放在课堂上,仅此而已。好问题,真的很有趣。
【解决方案3】:

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

// 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,那么您将具有循环依赖关系。


除了解决循环依赖之外,离线定义函数(无论是否是模板),将声明保留在一个可以作为目录有效工作的形式中,这比散布在标题中的声明更容易让程序员阅读充满定义。当您使用提供结构化标题概览的专用编程工具时,这种优势就会减弱。

【讨论】:

    【解决方案4】:

    我倾向于总是合并它们——但如果它们相互依赖,你就不能这样做。对于常规代码,您通常将代码放在 .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 文件中通过显式实例化来实现模板。
    • 对于非模板代码,您主要需要用户在标头中的类定义,以及在某些可编译单元中的实现,因此拆分是有意义的。对于模板代码,两者都需要在标头中,以便该参数下降。并不意味着人们无论如何都不会这样做......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-10-19
    • 1970-01-01
    • 1970-01-01
    • 2012-08-14
    • 1970-01-01
    • 2016-06-23
    • 1970-01-01
    相关资源
    最近更新 更多