Brad Hallisey
Brad Hallisey

Reputation: 33

Template Specialization: Const& versus Const*

The following code works as expected and prints:

Hello

World End

class TemplateTester
{
public:

    template<class T>
    void Print(T& obj) { obj.PrintNewline(); }

    template<class T>
    void Print(T* obj) { obj->Print(); /*obj->Test();*/ }
};

class Printer
{
public:
    void PrintNewline() const   { std::cout << m_string.c_str() << "\r\n"; }
    void Print() const          { std::cout << m_string.c_str(); }

    void Test() { m_string = "Oops";  }

    std::string m_string;
};

int main()
{
    Printer print1, print2;
    print1.m_string = "Hello";
    print2.m_string = "World";

    TemplateTester tester;
    tester.Print(print1);
    tester.Print(&print2);

    std::cout << " End ";

    return 0;
}

However, if you change the Print functions to

void Print(const T& obj)

void Print(const T* obj)

it always prefers the const reference flavor.

I read up a bit on Template Argument deduction but nothing jumped out at me.
Can anyone explain this to me and/or suggest a reasonable work around?

Upvotes: 2

Views: 124

Answers (3)

Brad Hallisey
Brad Hallisey

Reputation: 33

Putting together things from a few answers. I think I understand why this happens now and why promoting the pointer to const pointer doesn't work as it would with a normal function call.

If I understand @Jarod42 response correctly, it is because T is being evaluated in the template as Printer* not as Printer! Which makes sense in retrospect. It then decays T into Printer& or Printer* for evaluation inside the scope block but happens after it has chosen the function signature.

Here is a chunk of code that brings it all together. It includes some examples of different constness since there was some earlier confusion about that. Thanks for all the contributions everyone!

#include <iostream>

template <typename T>
void Print(T& obj) { obj.Print(); }

template <typename T>
void Print(T* obj) { obj->Print(); }

template <typename T>
void PrintNewline(const T& obj) { obj.PrintNewline(); }

template <typename T>
void PrintNewline(const T* obj) { obj->PrintNewline(); }

template <typename T>
void BetterPrint(const T& obj, std::false_type) { obj.PrintNewline(); }

template <typename T>
void BetterPrint(const T* obj, std::true_type) { obj->PrintNewline(); }

template <typename T>
void PrintChooser(T&& obj) { BetterPrint(obj, std::is_pointer<std::decay_t<T>>{}); }


class Printer
{
public:
    void Print() const          { std::cout << m_string.c_str(); }
    void PrintNewline() const   { std::cout << m_string.c_str() << "\n"; }

    void Test() { m_string = "Oops";  } // Not const!

    std::string m_string;
};


void Test1(const Printer* val)
{ 
    val->Print();
//    val->Test();  // Object type is const Printer.
    val = nullptr;
}

void Test2(Printer const* val)
{
    val->Print();
//    val->Test();  // Object type is const Printer.
    val = nullptr;
}

void Test3(Printer* const val)
{
    val->Print();
    val->Test();
//    val = nullptr;    // you cannont assign to a value that is const
}


int main()
{
    Printer print1, print2;
    print1.m_string = "Hello";
    print2.m_string = "World";

    Print(print1);  // Print(T&) as expected.
    Print(&print2); // Print(T*) as expected.
    PrintNewline(print1); // PrintNewline(const T&) as expected
//    PrintNewline(&print1); // PrintNewline(const T&) but expected PrintNewline(const T*) - Won't compile

    const Printer* print2Ptr = &print2; // Making user of your API do this is first isn't a happy place.
    PrintNewline(print2Ptr); // PrintNewline(const T*) as expected.

    PrintChooser(print1);
    PrintChooser(&print2);  // Happiness

    Test1(&print1); // Promotes correctly
    Test2(&print1); // Promotes correctly
    Test3(&print1); // Promotes correctly

    Test1(print2Ptr); // Pointer const matches
    Test2(print2Ptr); // Pointer const matches
//    Test3(print2Ptr); // Fails to compile, funciton takes a pointer to a non-cost object.


    return 0;
}

Upvotes: 0

Mojomoko
Mojomoko

Reputation: 529

Your problem is not connected to templates, but to pointer semantics and function overloading.

That is because you ask in the second case for a pointer to a const object. Which means that the pointer would have read-only rights. By passing the reference of a non-const object you pass something like a pointer value which has read/write rights - But you have no function overload for such a pointer. If you meant to use read-only pointers you must declare them as such:

const T* ptr = ...
T* const ptr = ...

Before passing them to your function.

Notice that such a pointer is needed if it points to a const object. But that such a pointer could be reassigned to a non-const object (but still having only read-only access). Especially does such a pointer do not turn your object into a const object - only protecting the object from changes through such a pointer...

a work around would be either introducing a third function:

template<T>
Print(T* obj){ 
  const T* ptr = obj;
  Print(ptr);
}

as aschepler suggested. Or changing your pointer function to take any pointer and use the const reference function:

template<T>
Print(T* obj){
  Print((*obj));
}

But I would argue that those work arounds obscure your interface as it is not obvious what happens with your object from your function signatures alone. Some problem you wouldn't have with your variant, forcing your users to use read-only pointer.

Upvotes: 2

Jarod42
Jarod42

Reputation: 217810

With

template<class T> void Print(const T& obj); // #1
template<class T> void Print(const T* obj); // #2

For

TemplateTester tester;
tester.Print(print1);  // call #1 with T=Printer (#2 is not viable)
tester.Print(&print2); // call #1 with T=Printer* (#1 is a better match than #2)

As you can see in (not trivial) overload_resolution rules, #1 is a better match than #2 for Printer*:

Printer* -> Printer* const & (exact match)
versus
Printer* -> const Printer*. (conversion)

There are several workarounds, for example tag dispatching:

class TemplateTester
{
private:
    template <typename T>
    void Print(const T& obj, std::false_type) {obj.PrintNewline();}

    template <typename T>
    void Print(const T* obj, std::true_type) {obj->Print(); /*obj->Test();*/}
public:
    template <typename T>
    void Print(T&& obj) {
        Print(obj, std::is_pointer<std::decay_t<T>>{});
    }
};

Demo

Upvotes: 4

Related Questions