Я задавался вопросом, есть ли какие-либо преимущества объявления функции шаблона вне линии против класса.
Я пытаюсь получить четкое представление о плюсах и минусах двух синтаксисов.
Вот пример:
Из линии:
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
в некоторых местах с первым фрагментом кода, но я не уверен.
РЕДАКТИРОВАТЬ: я знаю, нет разницы в том, как эти функции работают, что это в основном дело вкуса. Я хочу посмотреть, как оба синтаксиса играют с разными методами, и преимущество одного над другим. Я вижу в основном ответы, которые благоприятствуют друг другу, но я действительно хочу получить обе стороны. Более объективный ответ был бы лучше.
Существуют ли языковые функции, которые проще использовать с первой или второй версией?
Довольно тривиальный случай, но стоит упомянуть: специализаций.
Например, вы можете сделать это с помощью внепланового определения:
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
только для нескольких конкретных типов.
Он просто не может сделать это с помощью определений в классе.
Наконец, внеплановые определения помогают разбить круговые зависимости.
Это уже упоминалось в большинство другие ответы и не стоит приводить другой пример.
Отделение объявления от реализации позволяет вам сделать это:
// 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
тогда у вас будет круговая зависимость.
Помимо разрешения циклических зависимостей, определение функций (будь то шаблон или нет) вне строки оставляет объявления в форме, которая эффективно работает как оглавление, которое программистам легче читать, чем объявлениям, разбросанным по заголовку, полному определений , Это преимущество уменьшается, когда вы используете специализированные инструменты программирования, которые обеспечивают структурированный обзор заголовка.
Нет никакой разницы между двумя версиями относительно аргументов шаблона по умолчанию, 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 не может быть неполным типом в точке этого определения, так как компилятор должен знать, что 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
и параметры шаблона по умолчанию выглядят как для обоих вариантов. Объявления выглядят точно так же, только для определений вы должны отбросить параметры по умолчанию, если они есть.
Параметры шаблона по умолчанию
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
}
};
против
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
}
};
против
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.
СФИНА в типе возврата
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
}
На этот раз, поскольку нет параметров по умолчанию, объявление и определение на самом деле выглядят одинаково.
Я склонен всегда объединять их — но вы не можете сделать это, если они зависят от кода. Для обычного кода вы обычно помещаете код в файл .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% исправляет это каждый раз.
Один из вариантов — сделать один из них внутренним классом другого. Внутренний класс может выходить во внешний класс за пределами его собственной точки определения функций, поэтому проблема является своего рода скрытой, что может использоваться в большинстве случаев, когда у вас есть эти классы, зависящие от кода.