Is this a kosher way to work around the lack of function template default arguements?

Started by
6 comments, last by phresnel 14 years, 8 months ago
As you're probably aware, function templates in C++ don't support default arguments, which is to say that:
template < typename T = float >
T add(const T &lhs, const T &rhs) { return lhs + rhs; }
doesn't compile, because we're attempting to say "by default, assume a float." This, of course, extends to member functions as well. In the instance above, we could simply allow template type inference to do its thing. However, in some code I've recently written, the parameter that I'm using for said inference is being used for inference alone -- which means I'm instantiating the whole object, passing it to my function, and then only taking its type.
template < typename derived_type >
void some_action(int x, int y, int z, derived_type type)
{
  // the derived_type instance 'type' is never used, only its type 'derived_type'
  base_type * item = new derived_type();
}
If the type has a trivial constructor, this isn't a deal-breaker, but since it's client-facing code I cannot guarantee this will always be the case. It also seems foolish to require client types to provide a special, trivial constructor as a work-around. Furthermore, passing an instance of the object may leave clients with the impression that data/values are being taken from the instance, which isn't the case. Finally, trivial constructor or not, the potential overhead, both run-time and in writing the trivial constructors, is not in the spirit of C++ or the greater codebase. I'd rather just avoid the whole can of worms. Foregoing template type inference would cleanly solve all of these issues, albeit with slightly different syntax:
// Using template type inference:
some_action(1, 2, 3, some_derived_type());

// W/out template type inference:
some_action<some_derived_type>(1, 2, 3);
Now we're providing only the type, as we really wanted in the first place, with no risk of expensive constructors, undue work-arounds, or misunderstandings. The last hurdle is that it's kind of inconvenient for clients to have to specify the derived_type template function specifically -- especially when it its going to be the same predictable thing 90% of the time (I should mention now that some_action is, in fact, a member function of 'some_class' which itself takes, among others, 'base_type' as a template argument):
template < /* some other arguments /* typename base_type >
class some_class
{
public:
  template < typename derived_type >
  some_action(int x, int y, int z);

private:
  /* blah blah */
};
As you can see, giving 'derived_type' a default value of 'base_type' would neatly have given me the flexibility to specify any derived type, while also giving me a convenient syntax for the common case:
// instead of some_action<base_type>(1, 2, 3):
some_action(1, 2, 3);
As a work around, I implemented a non-template version of some_action, having the same return type and taking the same parameters as the template version, which simply forwards the call to the template version, but specifying 'base_type' as the template argument:
void some_action(int x, int y, int z) { some_action<base_type>(x, y, z); }
This gives me the syntax I want, but having two functions with what appears to be the same signature (aside from the template specification on one, which I realize makes the signatures different, or at least sufficiently distinguishable to the compiler, in reality) gave me a bit of an uneasy feeling. On the other hand, this type of work-around seems like it would be common practice, I even daresay an honest-to-goodness idiom -- but, before I move forward, I just wanted to appeal the the C++ gurus here to ask whether this is as good as it appears to be. So, Gurus, I ask you: is this standard-conforming C++, or are some compilers going to complain about a duplicated function signature? Is there anything in this which might cause problems in some unforeseen way? It seems to work under Visual C++ 2005 professional, but I don't have access to other platforms at the moment. Also, does anyone have any clue as to the rationale for C++ not supporting default arguments to template parameters on functions?

throw table_exception("(? ???)? ? ???");

Advertisement
If you have a copy of the language standard handy, look up the section on template overload resolution, which also refereces overload resolution.


You can use the function you provided along side the template-deduction version. It will add all the possible forms to overload resolution, and pick the best one or fail due to ambiguity.

If the choice is between a non-template function and a matching implicitly-selected template function, the non-template function will be chosen.


For your some_action() function:

some_action( int, int, int) ==> non-template function exactly matches
some_action( char, char, char) ==> template function because it requires no conversions
some_action( float, float, float) ==> template function because it requires no conversions
some_action( int, int, char) ==> non-template function, since not all parameters are base_type.
some_action( int, double, char) ==> non-template function, since not all parameters are base_type.
some_action ( classA, classA, classA) ==> template function
some_action ( classA, classB, classC) ==> non-template version if the classes can be converted to int, otherwise unresolved error.

The standard gives this example:
Quote:
 template<class T> T max(T a, T b) { return a>b?a:b; } void f(int a, int b, char c, char d) {  int m1 = max(a,b); // max(int a, int b)  char m2 = max(c,d); // max(char a, char b)  int m3 = max(a,c); // error: cannot generate max(int,char) } 

Adding the nontemplate function
 int max(int,int); 

to the example above would resolve the third call, by providing a function that could be called for max(a,c) after using the standard conversion of char to int for c.


There should be no problems with creating both, although it may give a little confusion to the users if they start using the functions with their own classes and don't really understand what's going on.
Quote:Original post by frob
If you have a copy of the language standard handy, look up the section on template overload resolution, which also refereces overload resolution.


Thanks for the pointer on what, specifically, to look at. I don't own a copy myself, but I imagine there must be something online, or failing that, at least I have the correct terms for google, and something to look up in my copy of The C++ Programming Language.

Quote:You can use the function you provided along side the template-deduction version. It will add all the possible forms to overload resolution, and pick the best one or fail due to ambiguity.

If the choice is between a non-template function and a matching implicitly-selected template function, the non-template function will be chosen.

For your some_action() function:

some_action( int, int, int) ==> non-template function exactly matches
some_action( char, char, char) ==> template function because it requires no conversions
some_action( float, float, float) ==> template function because it requires no conversions
some_action( int, int, char) ==> non-template function, since not all parameters are base_type.
some_action( int, double, char) ==> non-template function, since not all parameters are base_type.
some_action ( classA, classA, classA) ==> template function
some_action ( classA, classB, classC) ==> non-template version if the classes can be converted to int, otherwise unresolved error.

The standard gives this example:
Quote:
 template<class T> T max(T a, T b) { return a>b?a:b; } void f(int a, int b, char c, char d) {  int m1 = max(a,b); // max(int a, int b)  char m2 = max(c,d); // max(char a, char b)  int m3 = max(a,c); // error: cannot generate max(int,char) } 

Adding the nontemplate function
 int max(int,int); 

to the example above would resolve the third call, by providing a function that could be called for max(a,c) after using the standard conversion of char to int for c.


There should be no problems with creating both, although it may give a little confusion to the users if they start using the functions with their own classes and don't really understand what's going on.


Here I think we've become a little out-of-sync. The case I'm talking about here isn't actually templated on any of the parameter types any longer -- it was before, with a dummy parameter, in order to get type inference, but I believe that to be a poor design for the reasons outlined in the original post.

To be specific, the case I'm interested in is the choice between an explicitly-selected template function and a non-template function who's signature otherwise matches. In other words, given the two functions:
<template T>void some_func(int X); // Template version, so that we have the type T in the body.void some_func(int X); // Non-template version, use some explicit default instead.

I just wanted to know for sure that these two functions can peaceably co-exist as a matter of the standard.

It's apparent that for any explicitly given template type call (eg some_func<float>(10);) that the template version will be called. It seems equally apparent that when no template type is explicitly given, eg (some_func(10);) that the non-template version will be called since it matches exactly. Basically, the non-template version is called unless a template arguement to the function is explicitly given -- this is the behavior I want, I just want to be sure its standard, and not some quirk of Microsoft's compiler.

throw table_exception("(? ???)? ? ???");

Quote:Original post by Ravyne
Basically, the non-template version is called unless a template arguement to the function is explicitly given


Note that the compiler will deduce the type of the template argument when possible, so you don't have to write down template arguments explicitly in every case.

E.g.:
   #include <iostream>   template <typename T> void foo (T f) {           std::cout << "foo<T>(T)\n";   }   void foo (int f) {           std::cout << "foo(int)\n";   }   int main () {           foo<float> (5.0f);           foo (5.0f);           foo<int> (5);           foo (5);   }
will print
   foo<T>(T)   foo<T>(T)   foo<T>(T)   foo(int)


I'll try to find the right chapter from Vandevoorde.



update:




update:
Crap, Appendix B is incomplete and I don't have my copy nearby :/



update:
ISO/IEC 14882:2003, Section 13.3.1, Candidate functions and argument lists, Item 7 states:
Quote:In each case where a candidate is a function template, candidate function template specializations are generated using template argument deduction (14.8.3, 14.8.2). Those candidates are then handled as candidate functions in the usual way


So a candidate is deduced from the template, and is then subject to overload resolution as normal functions.

[Edited by - phresnel on August 19, 2009 2:30:43 AM]
Quote:Original post by Ravyne
As you're probably aware, function templates in C++ don't support default arguments, which is to say that:
template < typename T = float >T add(const T &lhs, const T &rhs) { return lhs + rhs; }

doesn't compile, because we're attempting to say "by default, assume a float." This, of course, extends to member functions as well.
error C4519: default template arguments are only allowed on a class template

Well I've learnt something today already. I've used it on classes before, including in ways similar to CComSafeArray, but It never occurred to me that it wouldn't extend to functions as well. Not that I've ever wanted to of course.
"In order to understand recursion, you must first understand recursion."
My website dedicated to sorting algorithms
Another workaround comes to mind:

    template <typename T> void foo (T) {}    void foo (int) {        foo<int> ();    }


Sidenote: I only see sense in it in the case where not all template arguments are part of the function's signature (and hence cannot be deduced automatically).
Phresnel: Exactly -- This is the solution I came to, but just wanted to be sure it was Kosher C++. In my case I required an additional type to be specified but there was no parameter that carried the desired type.

For what its worth, this is part of my AssetCache (most people call it a ResourceManager) -- the class caries a template parameter that is the type of asset being handled, and which may potentially be used as a base class. The method to acquire a resource can have a template parameter specified explicitly in order to create the new resource entry as a type derived from the base asset type. There is also a non-template function of an otherwise identical signature which handles the common case of the asset type without requiring the user to specify the base class explicitly.

I originally had a dummy parameter used to carry the type so it may be infered, but that carried a lot of problems with it, so I sought for a better solution. In the end, I'm happy that this solution gets rid of the problems associated with the dummy parameter, the syntax is nicer, and it even saves a couple keystrokes as well.

Its all up there somewhere... I admit it may be hard to digest the entirety of my posts sometimes [grin]

throw table_exception("(? ???)? ? ???");

;)

I think that method is not less kosher (prolly it's even kosherer) than putting functions into structs (example below), and there should be exactly no performance penalty, so why not?

template<typename T=int> struct Foo {        static void fun () {        }};int main () {        Foo<>::fun();}

This topic is closed to new replies.

Advertisement