C++ enforce second-pass name lookup in template function
Asked Answered
S

3

8

Is there some way to force C++ compilers to perform name lookup for a given symbol during template instantiation (and not before)?

Given the following code:

template <class T>
auto wrapper( T t ) -> decltype( f( t ) )
{
    return f( t );
}

unsigned char f( int x ) { return x % 256; }
unsigned char f( unsigned char x ) { return x; }

int main( int, char ** )
{
    auto x = wrapper( 3100 );
    return 0;
}

Is there anything I can do (apart from moving the definition of f to the top) in order to make that code compile and give the same results as if all definitions of f were available before the definition of wrapper?

I could not find anything, probably because I do not know how to phrase this question properly. All argument types of f can be assumed to be user-defined types, if this helps.

Shanly answered 28/1, 2015 at 7:52 Comment(3)
Why don't you use template specialization?Cutis
@Chiel: Thanks, great idea! So I would replace f with a class template? That works, but I had hoped for a solution that would not require me to change the definition of f (a lot). Or did you have something else in mind?Shanly
You can use a default that applies to most cases and specialization where required, see my answer.Cutis
J
2

Is there some way to force C++ compilers to perform name lookup for a given symbol during template instantiation (and not before)?

Yes. First of all, the name must be dependent. The name f in wrapper when used as f(t) is dependent because t is type-dependent. [temp.dep]/1:

In an expression of the form:

        postfix-expression ( expression-list opt )

where the postfix-expression is an unqualified-id, the unqualified-id denotes a dependent name if

  • any of the expressions in the expression-list is a pack expansion (14.5.3),
  • any of the expressions in the expression-list is a type-dependent expression (14.6.2.2), or
  • if the unqualified-id is a template-id in which any of the template arguments depends on a template parameter.

The problem is that names declared after the template itself, i.e. only in the instantiation but not the definition context, can solely be found using argument dependent name lookup. Your f overloads only take fundamental types, but those do not have the global namespace associated with them according to [basic.lookup.argdep]/2:

If T is a fundamental type, its associated sets of namespaces and classes are both empty.

Thus the fs you declared can never be found if the arguments are of the same type as the parameters. A little trick can help:

template <typename T>
struct refwrap
{
    T&& t;
    refwrap(T&& t) : t(std::forward<T>(t)) {}
    operator T&&() {return std::forward<T>(t);}
};

template <typename T>
auto make_refwrap( T&& t ) -> refwrap<T> // making use of reference collapsing
{ return {std::forward<T>(t)}; }         // inside refwrap to get forwarding

This template, when declared in the global namespace, will cause ADL to consider it. Rewrite wrapper as follows:

template <class T>
auto wrapper( T t ) -> decltype( f( make_refwrap(t) ) )
{
    return f( make_refwrap(t) );
}

Demo. This is not the proper way to do it though, as it will fail in more complex scenarios.

Jamajamaal answered 28/1, 2015 at 9:11 Comment(2)
Ah, now I see what the original problem was that lead to asking this question. My f function was defined in a different namespace (i.e. the same as wrapper) then the argument.Shanly
Which clause says it can only be found with ADL? Also, what would be the "proper" way to do it?Lopez
C
1

This would work with template specialization. Note that you have to decide what the default function is, because i cannot see it in question.

// default function
template <class T>
unsigned char f( T x ) { return x; }

// specialization for int
template <>
unsigned char f( int x ) { return x % 256; }

int main( int, char ** )
{
    auto x = f( 3100 );
    return 0;
}
Cutis answered 28/1, 2015 at 8:8 Comment(4)
My example was too simple and could be misinterpreted easily. Please do not assume that there is a default case. Please do not remove the wrapper function, because the question really is about that function, which, of course, is more complex than the identity function in my situation.Shanly
Why is simple overloading not sufficient?Cutis
The wrapper function is supposed to use another function f to get an intermediate result and to transform that result. In order to define wrapper all definitions of f must be available at the time wrapper is defined, because names are looked up when parsing the template definition. This is undesired behaviour in my case, because users of the library are supposed to be able to provide additional definitions of f and I'd have a hard time to tell them that they must include their definitions before they include certain other files.Shanly
But can you not use a container with function pointers, where users can register functions? And are functions registered compile time or run time?Cutis
S
0

The following code is not quite clean, but illustrates how class template specialization can be used in order to solve the problem. It maintains the original interface (i.e. f and wrapper can be used in the same way as before).

Thank you for giving me the right hints. I am open for a solution that is less verbose though.

#include <type_traits>

template <class ...>
struct F;

template <class T>
auto wrapper( T t )
    -> decltype( F<typename std::decay<T>::type>::f( t ) )
{
    return F<typename std::decay<T>::type>::f( t );
}

template <>
struct F<unsigned char>
{
    static unsigned char f( unsigned char x ) { return x; }
};

template <>
struct F<int>
{
    static unsigned char f( int x ) { return x % 256; }
};

template <class T>
auto f( T t )
    -> decltype( F<typename std::decay<T>::type>::f( t ) )
{
    return F<typename std::decay<T>::type>::f( t );
}

int main( int, char ** )
{
    auto x = wrapper( 3100 );
    return 0;
}
Shanly answered 28/1, 2015 at 8:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.