代码之家  ›  专栏  ›  技术社区  ›  Guillaume Racicot

模板函数与类内函数的离线定义

  •  40
  • Guillaume Racicot  · 技术社区  · 8 年前

    下面是一个例子:

    越界:

    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 在一些地方,第一个代码片段,但我不确定。

    编辑:我知道这些功能的工作方式没有区别,这主要是一个品味问题。我想看看两种语法如何使用不同的技术,以及它们之间的优势。我看到的答案大多是对彼此有利的,但我真的想得到双方的支持。更客观的答案会更好。

    4 回复  |  直到 8 年前
        1
  •  20
  •   Corristo    8 年前

    这两个版本在默认模板参数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 .

    表现力

    但是,区分线外和类内定义的不仅仅是可读性的提高。虽然每个类内定义都可以很容易地移动到行外定义,但相反的情况并非如此。一、 e.线外定义比类内定义更具表达力。当您拥有依赖于彼此功能的紧密耦合类时,会发生这种情况,因此前向声明不够。

    例如,如果您希望命令模式支持命令链接,则可以使用命令模式 让它支持用户定义的函数和仿函数,而不必从某些基类继承。如此 Command 本质上是一个“改进”版本 std::function .

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

    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 ? 最简单的方法是使用一个帮助器类来存储原始 命令 命令 然后执行,并按顺序调用两个调用运算符:

    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<T,R> Command<R, U> 实现调用运算符及其大小,因此转发声明在这里是不够的。即使要按指针存储成员命令,对于 operator() 命令 .

    有了这个助手,我们可以实现 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 命令 必须在声明之前 CommandNextHelper ,这意味着您无法定义 类中的函数。它的定义必须在 CommandNextHelper .

    运算符() operator[]

    结论

    因此,总结一下:这主要是你喜欢哪一种口味的问题,因为两者之间没有太大区别。只有在类之间存在循环依赖关系时,才能在所有成员函数的类定义中使用循环依赖关系。无论如何,我个人更喜欢离线定义,因为外包函数声明的技巧也可以帮助文档生成工具,如doxygen,它将只为实际类创建文档,而不为在另一个文件中定义和声明的其他助手创建文档。


    编辑

    如果我正确理解了你对原始问题的编辑,你想看看SFINAE, std::enable_if 这两种变体的默认模板参数看起来都一样。声明看起来完全相同,只是对于定义,如果有默认参数,则必须删除默认参数。

    1. 默认模板参数

      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 在默认模板参数中

      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

      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
      }
      

      同样,它只是缺少默认参数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
          }
      };
      

      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
      }
      

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

        2
  •  17
  •   Community CDub    8 年前

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

    这是一个非常琐碎的案例,但值得一提: 专业化 .

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

    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 of the other answers 不值得再举一个例子。

        3
  •  13
  •   eerorika    8 年前

    通过将声明与实现分离,可以执行以下操作:

    // 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 和其他声明,尽管 酒吧 巴兹 .

    如果函数模板是内联定义的,则必须包括 巴兹 申报前 酒吧 巴兹 取决于 酒吧 ,则会有循环依赖关系。


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

        4
  •  1
  •   dascandy    8 年前

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

    一种选择是将其中一个类作为另一个类的内部类。内部类可以延伸到外部类,超出其自身的函数定义点,因此问题是隐藏的,在大多数情况下,当您有这些相互依赖的类时,这是可用的。