Reputation: 33
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
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
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
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>>{});
}
};
Upvotes: 4