Reputation: 39294
A textbook I have notes that you can provide your own implementation for standard library functions like swap(x,y)
via template specialization or function overloading. This would be useful for any types which can benefit from something other than an assignment swap, like STL containers for example (which already have swaps written, I know).
My questions are the following:
What's better: template specialization to give your specialized swap implementation, or function overloading providing the exact parameters you wish to use without a template?
Why is it better? Or if they're equal, why is this?
Upvotes: 77
Views: 26766
Reputation: 54270
Short story: overload when you can, specialise when you need to.
Long story: C++ treats specialisation and overloads very differently. This is best explained with an example.
template <typename T> void foo(T);
template <typename T> void foo(T*); // overload of foo(T)
template <> void foo<int>(int*); // specialisation of foo(T*)
foo(new int); // calls foo<int>(int*);
Now let's consider this:
template <typename T> void foo(T);
template <> void foo<int*>(int*); // specialisation of foo(T)
template <typename T> void foo(T*); // overload of foo(T)
foo(new int); // calls foo(T*) !!!
The compiler does overload resolution before it even looks at specialisations. So, in both cases, overload resolution chooses foo(T*)
. However, only in the first case does it find foo<int>(int*)
because in the second case the int*
specialisation is a specialisation of foo(T)
, not foo(T*)
.
You mentioned std::swap
. This makes things even more complicated.
The standard says that you can add specialisations to the std
namespace. Great, so you have some Foo
type and it has a performant swap then you just specialise swap(Foo&, Foo&)
in the std
namespace. No problems.
But what if Foo
is a template class? C++ doesn't have partial specialisation of functions, so you can't specialise swap
. Your only choice is overloading, but the standard says that you aren't allowed to add overloads into the std
namespace!
You have two options at this point:
Create a swap(Foo<T>&, Foo<T>&)
function in your own namespace, and hope that it gets found via ADL. I say "hope" because if the standard library calls swap like std::swap(a, b);
then ADL simply won't work.
Ignore the part of the standard that says not to add overloads and do it anyway. Honestly, even though it's technically not allowed, in all realistic scenarios it's going to work.
One thing to remember though is that there's no guarantee that the standard library uses swap
at all. Most algorithms use std::iter_swap
and in some implementations that I've looked at, it doesn't always forward to std::swap
.
Upvotes: 109
Reputation: 210445
Note: I'm answering the question in the title. For the question in the body about swap
in particular, the answer is neither: either rely on move semantics, or use ADL.
There are lots of factors to consider. A few less-obvious ones that come to mind:
Allow me to illustrate:
inline long add(long a, long b) { return a + b; }
int main() { return add(0, 1L); } // OK
template<class T> T add(T a, T b);
template<> inline long add(long a, long b) { return a + b; }
int main() { return add(0, 1L); } // error: no matching function for call to 'add'
Essentially, every non-deduced type "occupies" the space of all types implicitly convertible to it, too. This is a subtle and key point to realize when designing the API, and it can come to bite you back when the function is actually used.
Crucially, notice there's no one-size-fits-all answer here; whether an implicit conversion makes sense for you depends on the API you're designing.
Some implicit conversions may be bad for reasons such as:
char const *
to std::string
)0
to int
instead of void *
)Some implicit conversions may be good for reasons such as:
f(std::true_type())
instead of f<bool>(std::true_type())
)std::string
to std::string_view
makes substr()
be O(1) instead of O(n))The only way to figure out the right answer here is to think through how your API could potentially be used, and to be ready to correct any mistakes in the future. The choice is by no means obvious, so don't expect to get it right.
C++20 kind of hinted at this principle when it started using base class definitions to for class template argument deduction (CTAD). While that's not about function templates, the principle still applies.
If you're planning to strength preconditions, relax postconditions, violate invariants, or anything else that might affect users adversely, think long and hard about specializations. You probably want to overload instead.
This of course may not be the sole deciding factor, given I've listed at least other considerations in this very answer that might affect your choice, but it's probably the biggest one. If you decide to go against it, it behooves you to document the choice and explain the rationale, because users will be surprised when your template blows up on them.
Overloads don't necessarily do that, though based on current language rules they sometimes might happen to do that as a side effect of there being no way to reference the base definition.
This may be a good thing or a bad thing, depending on why you're specializing.
If you're specializing because your new definition is slightly better in some way (e.g., maybe it's optimized), there's no significant reason to block the base definition. In fact, having it around may be helpful for testing the optimized definition. In such cases, overloading might make more sense than specialization.
If you're specializing because the base definition is unusable, wrong, or nonexistent, then you definitely don't want that definition to be accessible anywhere, and in that case, specialization is probably better than overloading.
Upvotes: 0
Reputation: 12757
There's little to add to Peter Alexander's answer. Let me just mention one use in wich function specialization could be prefearable over overloading: if you have to select among functions with no parameters.
E.g.
template<class T> T zero();
template<> int zero() { return 0; }
template<> long zero() { return 0L; }
To do something similar using function overloading, you would have to add a parameter to the function signature:
int zero(int) { return 0; }
long zero(long) { return 0L; }
Upvotes: 20
Reputation: 96241
You aren't allowed to overload functions in the std
namespace, but you are allowed to specialize templates (as I recall), so that's one option.
The other option is to put your swap
function in the same namespace as the thing it's operating on and using std::swap;
before calling an unqualified swap.
Upvotes: 8