Search code examples
c++templatestemplate-argument-deduction

Automatic template type deduction confusing pointers and references


Whilst trying to debug some code, I created a class to dump the values of a complicated hierarchy of objects to a text file so that I can compare a case where it works against a case where it doesn't. I implemented the class like this (reduced to a bare example):

#include <iostream>

class someOtherClass
{
public:
    someOtherClass()
        : a(0)
        , b(1.0f)
        , c(2.0)
    {}
    int a;
    float b;
    double c;
};

class logger
{
public:
    // Specific case for handling a complex object
    logger& operator << ( const someOtherClass& rObject )
    {
        std::cout << rObject.a << std::endl;
        std::cout << rObject.b << std::endl;
        std::cout << rObject.c << std::endl;
        return *this;
    }

    // [other class specific implementations]

    // Template for handling pointers which might be null
    template< typename _T >
    logger& operator << ( const _T* pBar )
    {
        if ( pBar )
        {
            std::cout << "Pointer handled:" << std::endl;
            return *this << *pBar;
        }
        else
            std::cout << "null" << std::endl;
        return  *this;
    }

    // Template for handling simple types.
    template< typename _T >
    logger& operator << ( const _T& rBar )
    {
        std::cout << "Reference: " << rBar << std::endl;
        return *this;
    }
};

int main(int argc, char* argv[])
{
    logger l;
    someOtherClass soc;
    someOtherClass* pSoc = &soc;
    l << soc;
    l << pSoc;
    pSoc = nullptr;
    l << pSoc;
    return 0;
}

I was expecting to get the following output:

0
1
2
Pointer handled:
0
1
2
null

But what I actually get is:

0
1
2
Reference: 010AF7E4
Reference: 00000000

The automatic type deduction appears to be picking the reference implementation and setting the type to someOtherClass* rather than picking the pointer implementation. I'm using Visual Studio 2012.


Solution

  • In logger& operator << ( const _T& rBar ) type T can be a pointer type, so in order to work properly this template needs some restriction:

    template< typename _T , typename = typename ::std::enable_if_t<!std::is_pointer<_T>::value> >
    logger& operator << ( const _T& rBar )
    {
        std::cout << "Reference: " << rBar << std::endl;
        return *this;
    }
    

    Online compiler

    This is required because when templates are instantiated the const _T & pBar with _T = someOtherClass * variant will be proffered as conversion sequence required in this case will only include a reference binding which is considered an identity conversion while const _T* pBar variant with _T = someOtherClass will involve a copy-initialization.