Is there a good way to resolve the ambiguity between nullptr_t and pointer overloads?

Viewed 168

When using the literal 0 in C++, the compiler can't disambiguate between pointer and nullptr_t overloads of a function.

Code that illustrates the problem:

struct Bar {};

void foo(Bar*) {
  std::cout << "Bar*" << std::endl;
}

void foo(std::nullptr_t) {
  std::cout << "nullptr_t" << std::endl;
}

TEST(NullPtrTest, ambiguity) {
  foo(nullptr);  // OK
  foo(0);  // ERROR
}

With Visual Studio 2019:

error C2668: '`anonymous-namespace'::foo': ambiguous call to overloaded function
message : could be 'void `anonymous-namespace'::foo(std::nullptr_t)'
message : or       'void `anonymous-namespace'::foo(`anonymous-namespace'::Bar *)'
message : while trying to match the argument list '(int)'

With GCC 9:

Test.cpp: In member function ‘virtual void {anonymous}::NullPtrTest_ambiguity_Test::TestBody()’:
Test.cpp:425:8: error: call of overloaded ‘foo(int)’ is ambiguous
  425 |   foo(0);  // ERROR
      |        ^
Test.cpp:415:6: note: candidate: ‘void {anonymous}::foo({anonymous}::Bar*)’
  415 | void foo(Bar*) {
      |      ^~~
Test.cpp:419:6: note: candidate: ‘void {anonymous}::foo(std::nullptr_t)’
  419 | void foo(std::nullptr_t) {
      |      ^~~

What's a good way to fix this?

Things that I don't want to do:

  • Replace 0 literal with nullptr everywhere. There are just way too many instances in our legacy code base.
  • Add an int or long (or similar) overload. Since 0 would be the only valid integer value, you would have to add a runtime check, and it would be ugly.
  • Remove the nullptr_t overload and check the value in the pointer overload at runtime. This works, and isn't terrible, but it prevents us from having an optimized implementation for null constants.

Thanks!

Clarifications:

  • We're using C++14. However, these functions are used by a lot of code that predates C++11.
  • The particular use case is to provide an optimized constexpr implementation when nullptr or 0 is used. The general pointer implementation will still have to check for null values. The nullptr_t overload isn't really necessary, but it would be nice.
  • I'm mainly asking about this to see if there is some option that I'm not considering.
1 Answers

Templates were not on the exclusion list - make it a template, constrained to only accept Bar*:

#include <iostream>
#include <type_traits>

struct Bar {}; 

template <typename T>
typename std::enable_if<std::is_same_v<T, Bar>>::type foo(T*) {
  std::cout << "Bar*\n";
}   

void foo(std::nullptr_t) {
  std::cout << "nullptr_t\n";
}   

int main() {
  Bar b;
  foo(nullptr);
  foo(0);
  foo(&b);
}

I guess in practice your Bar* gets probably passed derived types, i.e., you may need a different condition in std::enable_if. If foo(Bar*) is somewhat substantial you may not want to implement in a header. However, as there is only one instantiation anyway you can put the implementation into a .cpp file and instantiate it explicitly or, alternatively, dispatch to an implementation function.

Related