c++ - 模板函數與類中的脫節定義



templates code-readability (3)

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

一個相當微不足道的案例,但值得一提: 專業化

例如,您可以使用外線定義執行此操作:

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的實現。
他根本無法用類內定義來做到這一點。

最後,外部定義有助於打破循環依賴。
在大多數 其他答案中已經提到過這一點,並且給出另一個例子並不值得。

https://src-bin.com

我想知道在類中聲明模板功能是否有任何優勢。

我試圖清楚地了解這兩種語法的優缺點。

這是一個例子:

不合時宜:

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 ,但我不確定。

編輯:我知道這些功能的工作方式沒有區別,這主要是品味問題。 我想看看兩種語法如何使用不同的技術,以及一種優於另一種的優勢。 我看到大多數答案都有利於一個人,但我真的很想得到雙方的支持。 更客觀的答案會更好。


Answer #1

兩個版本之間在默認模板參數,SFINAE或std::enable_if之間沒有區別,因為重載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;
};

那麼你將如何實現呢?那麼? 最簡單的方法是有一個輔助類,它存儲原始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聲明之後。

我知道這不是一個簡單的例子,但我想不出一個更簡單的例子,因為當你絕對必須將一些運算符定義為類成員時,這個問題大多會出現。 這主要適用於expession模板中的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. 默認模板參數中的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
    }
    

    這次,由於沒有默認參數,聲明和定義實際上看起來都是一樣的。


Answer #2

我傾向於總是將它們合併 - 但如果它們是相互依賴的,你就不能這樣做。 對於常規代碼,您通常將代碼放在.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%修復它。

一種選擇是使其中一個成為另一個的內部類。 內部類可以超出它自己的函數定義點而進入外部類,因此問題是隱藏的,在大多數情況下,當你擁有這些依賴於類的類時,它是可用的。





code-readability