时间:2016-11-02 13:52:58

标签: 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
}

上课时间:

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

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

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

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

4 个答案:

答案 0 :(得分:11)

将声明与实现分开允许您执行此操作:

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

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

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

答案 1 :(得分:11)

两个版本之间在默认模板参数,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?最简单的方法是使用一个辅助类来存储原始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的类声明必须在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
        }
    };
    

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

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

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

    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。

  5. 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 :(得分:6)

  

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

一个相当微不足道的案例,但值得一提:专业化

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

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 :(得分: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%修复它。

另一种选择是将其中一个作为另一个的内部类。内部类可以超出其自己的函数定义点而进入外部类,因此问题是隐藏的,在大多数情况下,当您拥有这些依赖于类的类时,它可以使用。