I wondered if there was any advantages of declaring templates function out of line vs in the class.
I'm trying to get a clear understanding of the pros and cons of the two syntax.
Here's an example:
Out of line:
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 in class:
template<typename T>
struct MyType {
template<typename... Args>
void test(Args... args) const {
// do things
}
};
Are there language features that are easier to use with the first or second version? Does the first version would get in the way when using default template arguments or enable_if? I would like to see comparisons of how those two cases are playing with different language features like sfinae, and maybe potential future features (modules?).
Taking compiler specific behavior into account can be interesting too. I think MSVC needs inline
in some places with the first code snippet, but I'm not sure.
EDIT: I know there is no difference on how these features works, that this is mostly a matter of taste. I want to see how both syntaxes plays with different techniques, and the advantage of one over the other. I see mostly answers that favors one over another, but I really want to get both sides. A more objective answer would be better.
There is no difference between the two versions regarding default template arguments, SFINAE or std::enable_if
as overload resolution and substitution of template arguments work the same way for both of them. I also don't see any reason why there should be a difference with modules, as they don't change the fact that the compiler needs to see the full definition of the member functions anyway.
One major advantage of the out-of-line version is readability. You can just declare and document the member functions and even move the definitions to a separate file that is included in the end. This makes it so that the reader of your class template doesn't have to skip over a potentially large number of implementation details and can just read the summary.
For your particular example you could have the definitions
template<typename T>
template<typename... Args>
void MyType<T>::test(Args... args) const {
// do things
}
in a file called MyType_impl.h
and then have the file MyType.h
contain just the declaration
template<typename T>
struct MyType {
template<typename... Args>
void test(Args...) const;
};
#include "MyType_impl.h"
If MyType.h
contains enough documentation of the functions of MyType
most of the time users of that class don't need to look into the definitions in MyType_impl.h
.
But it is not just increased readibility that differentiates out-of-line and in-class definitions. While every in-class definition can easily be moved to an out-of-line definition, the converse isn't true. I.e. out-of-line definitions are more expressive that in-class definitions. This happens when you have tightly coupled classes that rely on the functionality of each other so that a forward declaration doesn't suffice.
One such case is e.g. the command pattern if you want it to support chaining of commands and have it support user defined-functions and functors without them having to inherit from some base class. So such a Command
is essentially an "improved" version of std::function
.
This means that the Command
class needs some form of type erasure which I'll omit here, but I can add it if someone really would like me to include it.
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;
};
So how would you implement .then
? The easiest way is to have a helper class that stores the original Command
and the Command
to execute after that and just calls both of their call operators in sequence:
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;
};
Note that Command cannot be an incomplete type at the point of this definition, as the compiler needs to know that Command<T,R>
and Command<R, U>
implement a call operator as well as their size, so a forward declaration is not sufficient here. Even if you were to store the member commands by pointer, for the definition of operator()
you absolutely need the full declaration of Command
.
With this helper we can implement 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);
}
Again, note that this doesn't work if CommandNextHelper
is only forward declared because the compiler needs to know the declaration of the constructor for CommandNextHelper
. Since we already know that the class declaration of Command
has to come before the declaration of CommandNextHelper
, this means you simply cannot define the .then
function in-class. The definition of it has to come after the declaration of CommandNextHelper
.
I know that this is not a simple example, but I couldn't think of a simpler one because that issue mostly comes up when you absolutely have to define some operator as a class member. This applies mostly to operator()
and operator[]
in expession templates since these operators cannot be defined as non-members.
So to conclude: It is mostly a matter of taste which one you prefer, as there isn't much of a difference between the two. Only if you have circular dependencies among classes you can't use in-class defintion for all of the member functions. I personally prefer out-of-line definitions anyway, since the trick to outsource the function declarations can also help with documentation generating tools such as doxygen, which will then only create documentation for the actual class and not for additional helpers that are defined and declared in another file.
If I understand your edit to the original question correctly, you'd like to see how general SFINAE, std::enable_if
and default template parameters looks like for both of the variants. The declarations look exactly the same, only for the definitions you have to drop default parameters if there are any.
Default template parameters
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
}
enable_if
in default template parameter
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
as non-type template parameter
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
}
Again, it is just missing the default parameter 0.
SFINAE in return type
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
}
This time, since there are no default parameters, both declaration and definition actually look the same.