0

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.

  • You need a [mcve] – Passer By Sep 22 '17 at 09:43
  • @PasserBy: the code provided is self-contained and reproduces the issue – Mat Sep 22 '17 at 09:44
  • @Mat It is complete and verifiable, but not minimal. This sounds a bit picky but I honestly think it helps reduce time spent for all parties involved. The author reducing the problem to its minimal is faster than others – Passer By Sep 22 '17 at 09:46
  • @Mat [This](http://coliru.stacked-crooked.com/a/e655c89abd7b0df1) is minimal. It is undoubtedly easier to read through and understand – Passer By Sep 22 '17 at 09:50
  • @PasserBy: Thank you for your feedback. I originally boiled this code down to just the bare bones but decided it lacked sufficient context to aid in determining a solution. Your original link mentions that the code should be 'Minimal _and readable_' and I may have moved more towards the latter. I shall take your input on board for the next time I post a question. – Charles Blessing Sep 22 '17 at 12:20

2 Answers2

0

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.

user7860670
  • 35,849
  • 4
  • 58
  • 84
  • 2
    You should add why const reference is picked over const pointer in overload resolution – Passer By Sep 22 '17 at 09:47
  • Thanks for the explanation, and potential solution. Unfortunately, VS2012 doesn't have a complete C++11 implementation and it won't allow me to specify a default type on a function definition (only on template classes). Is there another way of adding a restriction? – Charles Blessing Sep 22 '17 at 10:44
  • 1
    You can add another overload - that accepts T* - it should be chosen over reference overload when pointer is not const – Artemy Vysotsky Sep 22 '17 at 10:53
  • @CharlesBlessing Maybe you can make a template function to template class conversion trick? That is leave only one template function and call from it a static method of template class instantiated with the same template parameters as a function `template logger& operator <<(T && pBar) {t_InputOperatorImpl::work(pBar); return(*this);}`. So instead of writing several template functions you write several `t_InputOperatorImpl` template specializations. – user7860670 Sep 22 '17 at 10:53
  • @ArtemyVysotsky makes a good point. In fact, all you need is `operator<<(T*)` and `operator<<(const T&)`, because `T*` can deduce a pointer to const type too. – aschepler Sep 22 '17 at 11:32
  • @ArtemyVysotsky Thanks - changing the pointer template to be non-const made it work for me! – Charles Blessing Sep 22 '17 at 11:45
0

Here are a few modifications and annotations which may help as this logging class grows and becomes more mature.

I have attempted to:

a) solve the initial problem of incorrect type deduction.

b) decouple the logger from the things being logged (otherwise your logger has to know about the entire application and all libraries).

c) provide a mechanism for easily allowing logging of any type, even if provided by a third-party library.

#include <iostream>

// I've put the logger and its helpers into a namespace. This will keep code tidy and help with
// ADL.
namespace logging 
{
    // define a general function which writes a value to a stream in "log format".
    // you can specialise this for specific types in std:: if you wish here
    template<class T> 
    void to_log(std::ostream& os, T const& value)
    {
        os << value;
    }

    // define a general function objects for writing a log-representation of tyoe T.
    // There are 2 ways to customise this.
    // a) provide a free function called to_log in the same namespace as your classes (preferred)
    // b) specialise this class.
    template<class T>
    struct log_operation
    {
        void operator()(std::ostream& os, T const& value) const
        {
            to_log(os, value);
        }
    };

    // specialise for any pointer
    template<class T>
    struct log_operation<T*>
    {
        void operator()(std::ostream& os, T* ptr) const
        {
            if (!ptr)
                os << "null";
            else
            {
                os << "->";
                auto op = log_operation<std::decay_t<T>>();
                op(os, *ptr);
            }
        }
    };

    // the logger is now written in terms of log_operation()
    // it knows nothing of your application's types
    class logger
    {
    public:

        // Template for handling any type.
        // not that this will also catch pointers.
        // we will disambiguate in the log_operation
        template< typename T >
        logger& operator << ( const T& rBar )
        {
            auto op = log_operation<std::decay_t<T>>();
            op(std::cout, rBar);
            std::cout << std::endl;
            return *this;
        }
    };
}

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

// someOtherClass's maintainer provides a to_log function
void to_log(std::ostream& os, someOtherClass const& c)
{
    os << "someOtherClass { " << c.a << ", " << c.b << ", " << c.c << " }";
}

namespace third_party
{
    // the is in a 3rd party library. There is no to_log function and we can't write one which will be found with
    // ADL...
    struct classWhichKnowsNothingOfLogs {};
}

/// ..so we'll specialise in the logging namespace

namespace logging
{
    template<>
    struct log_operation<::third_party::classWhichKnowsNothingOfLogs>
    {
        void operator()(std::ostream& os, ::third_party::classWhichKnowsNothingOfLogs const& value) const
        {
            os << "classWhichKnowsNothingOfLogs {}";
        }
    };
}


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

    l << third_party::classWhichKnowsNothingOfLogs();
    return 0;
}

expected output:

someOtherClass { 0, 1, 2 }
->someOtherClass { 0, 1, 2 }
null
classWhichKnowsNothingOfLogs {}
Richard Hodges
  • 68,278
  • 7
  • 90
  • 142