Argument-dependent lookup behaving unexpectedly on types aliased from another namespace
Asked Answered
D

1

7

I just ran into some interesting behavior with argument-dependent lookup, which I do not fully understand:

#include <iostream>

namespace a {
struct Foo {
  Foo(int v1, int v2) : v1(v1), v2(v2) { }
  int v1,v2;
};
}

namespace b {
template <typename T>
struct Baz : T {
  using T::T;
};
}

namespace c {
using Foo = ::b::Baz< ::a::Foo>;

// (1) NOT FOUND BY ADL
// std::ostream& operator << (std::ostream& os, const Foo& foo)
// {
//   return os << foo.v1 << "," << foo.v2;
// }
}

namespace b {

// (2) FOUND BY ADL
std::ostream& operator << (std::ostream& os, const ::c::Foo& foo)
{
  return os << foo.v1 << "," << foo.v2;
}

}

int main()
{
  c::Foo foo(1,2);
  // Variant (1): ADL fails: should it not find
  //   c::operator<<(std::ostream&, const Foo&) ?
  // Variant (2) compiles
  std::cout << "foo: " << foo << std::endl;
}

I get that c::Foo is actually b::Baz<...>, so it somewhat makes sense that ADL finds the operator when I define it inside namespace b. But it seems to defy intuition that defining the operator inside namespace c does not work, since c::Foo should (IMHO) allow the compiler to perform ADL inside namespace c as well.

Why is that not the case? What is the rationale behind this?

Derwent answered 4/5, 2016 at 11:56 Comment(2)
@mindriot: "But that seems like an implementation detail." No, it isn't. Nowhere in C++ are you allowed to use a type alias in a way that would behave in any way differently than if you used the actual type. That's not an "implementation detail"; that's required behavior for type aliases.Preciosa
@NicolBolas I agree, but that's not what I meant. Maybe I should rephrase: That particular explanation (“imagine the reference getting rewritten”) makes it sound like it’s just an implementation detail. The reason Columbo gave in his answer makes more sense.Derwent
U
4

[basic.lookup.argdep]/2:

Typedef names and using-declarations used to specify the types do not contribute to this set.

Neither typedef names nor using-declarations are affiliated with the type they designate. If they were (which would actually be counterintuitive, IMO), things would break very easily; just because I typedef'd some class, all calls are now considering functions added in the vicinity of the typedef, which is practically never desired:

#include <string>

namespace A {
    using str = std::string;
    int stoi(str); // This will be a candidate in e.g. stoi(std::string{"0.4"}),
                   // leading to potentially different behavior or ambiguity

namespace B {
    int stoi(std::string); // This is no candidate, because...?
}
Unstick answered 4/5, 2016 at 13:12 Comment(3)
Thanks, that's what I expected: they are explicitly left out. Do you have any idea about the rationale? For example, would anything break, or get overly complicated to implement, if this particular sentence had been left out?Derwent
Thank you, that makes sense!Derwent
It would also mean that a variable typed with the name A::str would behave differently to one typed with the name std::string, even though the two are supposed to be names for the same type. That's somewhat counter-intuitive.Relieve

© 2022 - 2024 — McMap. All rights reserved.