Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why can't my trait template class lookup operator<< for llvm::StringRef?

Following the question How can I detect if a type can be streamed to an std::ostream? I've written a trait class that says if some type can be streamed to an IO stream. The trait has seemed to work well until now that I've discovered a problem.

I'm using the code inside a project that uses LLVM and I'm using their StringRef class (which is similar in spirit to the proposed std::string_view). Here is a link to the Doxygen doc for the class, from where you can find it's declaration header file if needed. Since LLVM doesn't provide an operator<< to stream StringRef objects to std streams (they use a custom lightweight stream class), I've written one.

However, when I use the trait it doesn't work if my custom operator<< is declared after the trait (this happens because I have the trait in one header and the operator<< function in another one). I used to think that the lookup in template instantiations worked from the point of view of the instantiation point, so I thought it should work. Actually, as you can see below, with another class and its custom operator<<, declared after the trait, everything works as expected (that's why I've discovered this problem only now), so I can't figure out what makes StringRef special.

This is the complete example:

#include <iostream>

#include "llvm/ADT/StringRef.h"

// Trait class exactly from the cited question's accepted answer
template<typename T>
class is_streamable
{
   template<typename SS, typename TT>
   static auto test(int)
      -> decltype(std::declval<SS&>() << std::declval<TT>(),
                  std::true_type());

   template<typename, typename>
   static auto test(...) -> std::false_type;

public:
   static const bool value = decltype(test<std::ostream,T>(0))::value;
};

// Custom stream operator for StringRef, declared after the trait
inline std::ostream &operator<<(std::ostream &s, llvm::StringRef const&str) {
   return s << str.str();
}

// Another example class
class Foo { };
// Same stream operator declared after the trait
inline std::ostream &operator<<(std::ostream &s, Foo const&) {
    return s << "LoL\n";
}

int main()
{
   std::cout << std::boolalpha << is_streamable<llvm::StringRef>::value << "\n";
   std::cout << std::boolalpha << is_streamable<Foo>::value << "\n";

   return 0;
}

Contrary to my expectations, this prints:

false
true

If I move the declaration of the operator<< for StringRef before the trait declaration, it prints true. So why is this strange thing happening and how can I fix this issue?

like image 778
gigabytes Avatar asked Nov 10 '22 09:11

gigabytes


1 Answers

As mentioned by Yakk this is simply ADL: Argument Dependent Lookup.

If you don't want to bother, just remember that you should always write a free function in the same namespace as at least one of its arguments. In your case, since it's forbidden to add functions to std, it means adding your function into the llvm namespace. The fact that you needed to qualify the StringRef argument with llvm:: was a dead give away.

The rules of function resolution are fairly complex, but as a quick sketch:

  • name lookup: collects a set of potential candidates
  • overload resolution: picks the best candidate among the potentials
  • specialization resolution: if the candidate is a function template, check for any specialization that could apply

The name lookup phase which we are concerned with here is relatively simple. In short:

  • it scans the argument's namespaces, then their parents, ... until it reaches the global scope
  • then proceeds by scanning the current scope, then its parent scope, ... until it reaches the global scope

Probably to allow shadowing (like for any other name lookup), the lookup stops at the first scope in which it encounters a match, and haughtily ignore any surrounding scope.

Note that using directives (using ::operator<<; for example) can be used to introduce a name from another scope. It is burdensome though, as it puts the onus on the client, so please don't rely on its availability as an excuse for sloppiness (which I've seen done :x).


Example of shadowing: this prints "Hello, World" without raising an ambiguity error.

#include <iostream>

namespace hello { namespace world { struct A{}; } }

namespace hello { void print(world::A) { std::cout << "Hello\n"; } }

namespace hello { namespace world { void print(A) { std::cout << "Hello, World\n"; } } }

int main() {
    hello::world::A a;
    print(a);
    return 0;
}

Example of interrupted search: ::hello::world yielded a function named print so it was picked out even though it does not match at all and ::hello::print would have been a strictly better match.

#include <iostream>

namespace hello { namespace world { struct A {}; } }

namespace hello { void print(world::A) { } }

namespace hello { namespace world { void print() {} } };

int main() {
    hello::world::A a;
    print(a); // error: too many arguments to function ‘void hello::world::print()’
    return 0;
}
like image 188
Matthieu M. Avatar answered Nov 15 '22 08:11

Matthieu M.