Function signature-like expressions as C++ template arguments
Asked Answered
S

2

59

I was looking at Don Clugston's FastDelegate mini-library and noticed a weird syntactical trick with the following structure:

TemplateClass< void( int, int ) > Object;

It almost appears as if a function signature is being used as an argument to a template instance declaration.

This technique (whose presence in FastDelegate is apparently due to one Jody Hagins) was used to simplify the declaration of template instances with a semi-arbitrary number of template parameters.

To wit, it allowed this something like the following:

// A template with one parameter
template<typename _T1>
struct Object1
{
    _T1 m_member1;
};

// A template with two parameters
template<typename _T1, typename _T2>
struct Object2
{
    _T1 m_member1;
    _T2 m_member2;
};

// A forward declaration
template<typename _Signature>
struct Object;

// Some derived types using "function signature"-style template parameters
template<typename _Dummy, typename _T1>
struct Object<_Dummy(_T1)> : public Object1<_T1> {};

template<typename _Dummy, typename _T1, typename _T2>
struct Object<_Dummy(_T1, _T2)> : public Object2<_T1, _T2> {};

// A. "Vanilla" object declarations
Object1<int> IntObjectA;
Object2<int, char> IntCharObjectA;

// B. Nifty, but equivalent, object declarations
typedef void UnusedType;
Object< UnusedType(int) > IntObjectB;
Object< UnusedType(int, char) > IntCharObjectB;

// C. Even niftier, and still equivalent, object declarations
#define DeclareObject( ... ) Object< UnusedType( __VA_ARGS__ ) >
DeclareObject( int ) IntObjectC;
DeclareObject( int, char ) IntCharObjectC;

Despite the real whiff of hackiness, I find this kind of spoofy emulation of variadic template arguments to be pretty mind-blowing.

The real meat of this trick seems to be the fact that I can pass textual constructs like "Type1(Type2, Type3)" as arguments to templates. So here are my questions: How exactly does the compiler interpret this construct? Is it a function signature? Or, is it just a text pattern with parentheses in it? If the former, then does this imply that any arbitrary function signature is a valid type as far as the template processor is concerned?

A follow-up question would be that since the above code sample is valid code, why doesn't the C++ standard just allow you to do something like the following, which does not compile?

template<typename _T1>
struct Object
{
    _T1 m_member1;
};

// Note the class identifier is also "Object"
template<typename _T1, typename _T2>
struct Object
{
    _T1 m_member1;
    _T2 m_member2;
};

Object<int> IntObject;
Object<int, char> IntCharObject;
Stokes answered 9/1, 2011 at 21:51 Comment(3)
+1, great question and I'm a big fan of the library in question.Exorcism
See paragraph "C++ Function Types As A DSL" from this articleStine
For another way to emulate variadic template arguments see templog.svn.sourceforge.net/viewvc/templog/code/trunk/…Fiftyfifty
A
48

With regards to your first question - about the type int(char, float) - this is a valid C++ type and is the type of a function that takes in a char and a float and returns an int. Note that this is the type of the actual function, not a function pointer, which would be an int (*) (char, float). The actual type of any function is this unusual type. For example, the type of

void DoSomething() {
    /* ... */
}

is void ().

The reason that this doesn't come up much during routine programming is that in most circumstances you can't declare variables of this type. For example, this code is illegal:

void MyFunction() { 
    void function() = DoSomething; // Error!
}

However, one case where you do actually see function types used is for passing function pointers around:

void MyFunction(void FunctionArgument()) {
     /* ... */
}

It's more common to see this sort of function written to take in a function pointer, but it's perfectly fine to take in the function itself. It gets casted behind-the-scenes.

As for your second question, why it's illegal to have the same template written with different numbers of arguments, I don't know the exactly wording in the spec that prohibits it, but it has something to do with the fact that once you've declared a class template, you can't change the number of arguments to it. However, you can provide a partial specialization over that template that has a different number of arguments, provided of course that the partial specialization only specializes over the original number of arguments. For example:

template <typename T> class Function;
template <typename Arg, typename Ret> class Function<Ret (Arg)> { 
    /* ... */
};

Here, Function always takes one parameter. The template specialization takes in two arguments, but the specialization is still only over one type (specifically, Ret (Arg)).

Alessi answered 9/1, 2011 at 22:0 Comment(8)
Very nifty. The idea of a "type" of a function is pretty much outside of day-to-day C++ concept space, which is why this trick seemed so weird to me at first. But so if "void( int )" is a type, can one have, for example, a type which is a pointer to "void( int )"? Would that be the same thing as a function pointer?Stokes
Aha, hence the "correct" function pointer syntax, e.g. "void (*fp)() = & SomeFunction;". And also the dereferenced function call "(*fp)();" It seems kinda crappy to me that there are compilers that allow you to forgo the address-of and dereference operators...it makes the function type and function pointer type seem like the same thing, when they apparently are not.Stokes
@Jeff Lee- The compilers actually have to let you get away with this. The C++ ISO spec (section 4.3.1) mentions that any function can implicitly be converted into a pointer to that function. I think it's for C compatibility.Alessi
@Alessi - Does that contradict Clugston's claim in the linked article, or is he talking about something else: "Some compilers (most notably MSVC 6 and 7) will let you omit the &, even though it is non-standard and confusing. More standard-compliant compilers (e.g., GNU G++ and MSVC 8 (a.k.a. VS 2005)) require it, so you should definitely put it in."Stokes
In the context of raw functions you don't need the &, but for pointers-to-member-function the & is necessary. I'm not sure what Clugston meant in that article, but it seems like he was slightly mistaken. Just a guess.Alessi
Yeah, the context was instance method pointers, so it seems he actually agrees with you.Stokes
Although I agree you shouldn't use macros varargs, but why isn't VA_ARGS a part of C++? It was added in C++11Soughtafter
@sasha.sochka- At the time I wrote this, I didn't know that C++11 had added __VA_ARGS__; it wasn't supported in C++03. Thanks for letting me know about that... I'll update the answer accordingly.Alessi
C
5
int* int_pointer;    // int_pointer   has type "int*"
int& int_reference;  // int_reference has type "int&"
int  int_value;      // int_value     has type "int"

void (*function_pointer)(int, int);    // function_pointer has type
                                       // "void (*)(int, int)"
void (&function_reference)(int, int);  // function_reference has type
                                       // "void (&)(int ,int)"
void function(int, int);               // function has type
                                       // "void(int, int)"

template<>
struct Object1<void(int, int)>
{
    void m_member1(int, int);  // wait, what?? not a value you can initialize.
};
Cypress answered 9/1, 2011 at 22:1 Comment(1)
Right, that's definitely correct. Although in my example, the declaration that takes a function signature as a template parameter is not Object1. Rather, the declaration is being filtered through a derived class called "Object", which extracts individual types out of the signature.Stokes

© 2022 - 2024 — McMap. All rights reserved.