c++sfinaec++-conceptsoverload-resolutionpartial-specialization

Specialize function template for all types of pointers


I want to implement a streaming style logging lib.

I made a Log_t class to buffer a log entry, and do real output up on it being destroyed, like this:

class Log_t: public std::ostringstream {
    static int log_id;
public:
    Log_t() { ++log_id; };
    ~Log_t() override {
        // in order to simplify discussion, here output it to cout
        std::cout << "log" << log_id << ':' << str() << std::endl;
    };
};

int Log_t::log_id {};

template <typename T>
Log_t& operator<<( Log_t& log_, const T& body_ ) {
    static_cast<std::ostream&>( log_ ) << body_;
    return log_;
};

Whenever I have a customized class/struct, I can log it in same way, as long as I implemented a output function for it. Like this:

struct Response_t {
    int _id;
};

// Using std::ostream instead of Log_t, as it may be outputed to other targets
std::ostream& operator<<( std::ostream& os_, const Response_t& rsp_ ) {
    return os_ << "resp_id:" << rsp_._id;
};

Then I can log it as:

Response_t response {123};
Log_t() << "local obj=" << response;

But in fact Response_t mainly is returned by a lib, and in pointer form instead of a real ojb/ref, so I should probably output the contents that be pointed by instead of a address, and I have to check if it is a nullptr, before logging it. Furthermore, I don't wana check if it is a nullptr in every customized output functions, instead I want to check it only once in the logging function template. I changed the function template into these two:

template <typename T>
Log_t& operator<<( Log_t& log_, const T* body_ ) {
    if( body_ == nullptr )
        static_cast<std::ostream&>( log_ ) << "{nullptr}";
    else
        static_cast<std::ostream&>( log_ ) << *body_;
    return log_;
};

template <typename T>
Log_t& operator<<( Log_t& log_, const T& body_ ) {
    static_assert( ! std::is_null_pointer_v<T> );
    static_assert( ! std::is_pointer_v<T> );

    static_cast<std::ostream&>( log_ ) << body_;
    return log_;
};

The complete codes:

#include <iomanip>
#include <iostream>
#include <sstream>
#include <type_traits>

class Log_t: public std::ostringstream {
    static int log_id;
public:
    Log_t() { ++log_id; };
    ~Log_t() override { std::cout << "log" << log_id << ':' << str() << std::endl; };
};

int Log_t::log_id {};

/*
    Let's say that parameter body_ is a pointer received from another lib,
    so it may be a nullptr, and I don't wana check if it is a nullptr
    everywhere in my projects, instead I want to check it only once in the
    following function.
*/
template <typename T>
Log_t& operator<<( Log_t& log_, const T* body_ ) {
    if( body_ == nullptr )
        static_cast<std::ostream&>( log_ ) << "{nullptr}";
    else
        static_cast<std::ostream&>( log_ ) << *body_;
    return log_;
};

template <typename T>
Log_t& operator<<( Log_t& log_, const T& body_ ) {
    static_assert( ! std::is_null_pointer_v<T> );
    static_assert( ! std::is_pointer_v<T> );

    static_cast<std::ostream&>( log_ ) << body_;
    return log_;
};

struct Response_t {
    int _id;
};
std::ostream& operator<<( std::ostream& os_, const Response_t& rsp_ ) {
    return os_ << "resp_id:" << rsp_._id;
};

int main() {
    Response_t response {123};
    Log_t() << "local obj=" << response;

    // Suppose rsp_ptr is received from elsewhere, may be nullptr!
    Response_t* rsp_ptr = &response;
    Log_t() << "ptr obj=" << rsp_ptr;
    rsp_ptr = nullptr;
    Log_t() << "ptr obj=" << response;

    exit( EXIT_SUCCESS );
};

But I can NOT pass through the compilation, and got this:

ostream-ptr.cpp:36:31: error: static assertion failed 36 |
static_assert( ! std::is_pointer_v );

It looks like the specialized version for pointers has been ignored by gcc-12.3?

Maybe I can resolve it with concept of c++20? How to code it?


Solution

  • The const T* overload isn't getting ignored. The const T& overload is just more specialized.

    Consider this example:

    template <typename T>
    void f(const T*) {}
    
    template <typename T>
    void f(const T&) {}
    
    int main() {
        int i;
        f(i); // calls the ref overload as expected, with T = int
        int* p = &i;
        f(p); // still calls the ref overload, but with T = int*
        const int* pc = &i;
        f(pc); // correctly calls the pointer overload, with T = int
    }
    

    Demo

    If you add a third overload void f(T*) without the const, then the second call (f(p)) uses that one.

    I assume you want the const T* overload to be called whenever a pointer is passed, const or not. In this case, in C++20, you can add a requires clause to disable the const T& overload:

    template <typename T>
    requires(!std::is_pointer_v<T>)
    Log_t& operator<<( Log_t& log_, const T& body_ ) { ... }
    

    Demo

    Before C++20, you can similarly use SFINAE.

    Also, note that the line rsp_ptr = nullptr; does not make rsp_ptr a nullptr in the sense that the type of rsp_ptr does not change to std::nullptr_t, which is what std::is_null_pointer checks. It only sets rsp_ptr to an integral value of zero.

    To test your log for actual std::nullptr_t values, you need to pass nullptr explicitly (or make a std::nullptr_t variable):

    Log_t() << "ptr obj=" << nullptr;
    

    This will fail, and so you may want to update the requires clause to:

    requires(!std::is_pointer_v<T> and !std::is_null_pointer_v<T>)
    

    You can also remove the static_asserts, since they just repeat the requires clause.