Подтвердить что ты не робот

Вне строки определения функции шаблона vs в классе

Я задавался вопросом, есть ли какие-либо преимущества объявления функций шаблонов из строки vs в классе.

Я пытаюсь получить четкое представление о плюсах и минусах двух синтаксисов.

Вот пример:

Вне строки:

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 в некоторых местах с первым фрагментом кода, но я не уверен.

EDIT: Я знаю, что нет никакой разницы в том, как эти функции работают, что это в основном вопрос вкуса. Я хочу видеть, как оба синтаксиса играют с разными приемами, и преимущество одного над другим. Я вижу в основном ответы, которые поддерживают друг друга, но я действительно хочу получить обе стороны. Более объективный ответ был бы лучше.

4b9b3361

Ответ 1

Существуют ли языковые функции, которые проще использовать с первой или второй версией?

Довольно тривиальный случай, но стоит упомянуть: специализации.

В качестве примера вы можете сделать это с использованием нестандартного определения:

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 только для нескольких конкретных типов.
Он просто не может этого сделать с определениями в классе.


Наконец, определения вне строки помогают разбить круговые зависимости.
Это уже упоминалось в большей части других ответов, и не стоит приводить другой пример.

Ответ 2

Отделив объявление от реализации, вы можете сделать это:

// 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, тогда у вас будет круговая зависимость.


Помимо разрешения круговых зависимостей, определение функций (независимо от того, шаблон или нет) вне линии, оставляет объявления в форме, которая работает эффективно как оглавление, что проще для программистов читать, чем объявления, разбрызгиваемые через заголовок полный определений. Это преимущество уменьшается, когда вы используете специализированные инструменты программирования, которые обеспечивают структурированный обзор заголовка.

Ответ 3

Нет никакой разницы между двумя версиями относительно аргументов шаблона по умолчанию, 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? Самый простой способ - иметь вспомогательный класс, который сохраняет исходные Command и Command для выполнения после этого и просто вызывает оба оператора вызова в последовательности:

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.

С помощью этого помощника мы можем реализовать 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[] в шаблонах исхода, поскольку эти операторы не могут быть определены как нечлены.

Заключение

Итак, в заключение: в основном речь идет о вкусе, который вы предпочитаете, поскольку между ними нет большой разницы. Только если у вас есть круговые зависимости между классами, вы не можете использовать in-class defintion для всех функций-членов. Я лично предпочитаю определения вне очереди, так как трюк для аутсорсинга деклараций функций также может помочь с инструментами создания документации, такими как doxygen, которые затем создадут документацию только для фактического класса, а не для дополнительных помощников, которые определены и объявлены в другом файле.


Изменить

Если я правильно понял ваше редактирование исходного вопроса, вы бы хотели увидеть, как общие параметры SFINAE, std::enable_if и параметры шаблона по умолчанию выглядят как для обоих вариантов. Декларации выглядят точно так же, только для определений вам нужно отбросить параметры по умолчанию, если они есть.

  • Параметры шаблона по умолчанию

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

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

    На этот раз, поскольку нет параметров по умолчанию, как декларация, так и определение фактически выглядят одинаково.

Ответ 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% исправляет это каждый раз.

Один из вариантов - сделать один из них внутренним классом другого. Внутренний класс может выйти во внешний класс за пределами своей собственной точки определения функций, поэтому проблема скрыта, что можно использовать в большинстве случаев, когда у вас есть эти связанные классы.