Difference between sibling namespaces in nested vs global context

Viewed 98

I have some C++ libraries that are written using nested namespaces. These libraries use many mathematical functions that, for readability, are better off read without explicitly specifying namespaces. Right now, a simplified version of the code would look like this.

namespace Base::Sibling1 {
  float cos(float x) { return cosf(x); }
};

namespace Base::Sibling2 {
  using namespace Sibling1;
  float f(float x) { return cos(x); }
};

We wanted to move to use flatter namespaces mostly to make it easier to extend the library with sibling code. We had hoped that a simple change like this would work:

namespace Sibling1 {
  float cos(float x) { return cosf(x); }
};

namespace Sibling2 {
  using namespace Sibling1;
  float f(float x) { return cos(x); }
};

Instead this fails now since Sibling2::f() calls cos(x) that is now ambiguous.

I have two questions

  1. why is it ambiguous now and not int the first version?
  2. is it possible to obtain the behavior we had before without listing all functions explicitly using using Sibling1::cos?
2 Answers

The problem cannot be reproduced as you describe it. Moreover, there is no reason that the flattening alone introduces ambiguities, if you use expose in the siblings the same names you introduced in Base. So, the root cause is probably in some parts of the code you are not showing.

The second version does not define cosf(), so you probably are using some namespace. If in that namespae there is also a cos() you create an ambiguity between the two overloads. I could reproduce this by using namespace std:

#include <iostream>
#include <cmath>
using namespace std;
namespace Sibling1 {
  float cos(float x) { return cosf(x); }
}
namespace Sibling2 {
  using namespace Sibling1;
  float f(float x) { return cos(x); }
}

Online demo: the compilation error indicates which are the candidates behind the ambiguity. If you now comment the using namespace out, the ambiguity goes away, and the code compiles. Online proof.

Namespaces are meant to avoid naming conflicts and keep control of ambiguities:

  • Creating a namespace and systematically using namespace everywhere defeats the whole purpose.
  • On the other side, long using lists are painful to maintain, especially if you have to repeat them in several sibling namespaces. This is probably why the Base and the nesting were created in the first place.

In the first version, you do not show what's in Base. It is possible that some more tailored using clauses are used therein instead of full namespaces: if selectively injecting the really required functions in the Base namespace, they are made available within the siblings avoiding ambiguity that can be introduced by injecting the name of unnecessary functions.

The ambiguity is due to how using directives work.

[namespace.udir] (emphasis mine)

2 A using-directive specifies that the names in the nominated namespace can be used in the scope in which the using-directive appears after the using-directive. During unqualified name lookup ([basic.lookup.unqual]), the names appear as if they were declared in the nearest enclosing namespace which contains both the using-directive and the nominated namespace. [ Note: In this context, “contains” means “contains directly or indirectly”. — end note ]

3 A using-directive does not add any members to the declarative region in which it appears. [ Example:

namespace A {
  int i;
  namespace B {
    namespace C {
      int i;
    }
    using namespace A::B::C;
    void f1() {
      i = 5;        // OK, C​::​i visible in B and hides A​::​i
    }
  }
  namespace D {
    using namespace B;
    using namespace C;
    void f2() {
      i = 5;        // ambiguous, B​::​C​::​i or A​::​i?
    }
  }
  void f3() {
    i = 5;          // uses A​::​i
  }
}
void f4() {
  i = 5;            // error: neither i is visible
}

— end example ]

So given your structure of namespaces, according to bit I made bold in paragraph 2, when you write

using namespace Sibling1;

It kinda translates to this

namespace /* Enclosing */ {

    using Sibling1::cos;

    namespace Sibling2 {
      float f(float x) { return cos(x); }
    };

}

The namespace I marked as enclosing is either Base or the global namespace. The "kinda" bit (according to paragraph 3) is that it's not an actual declaration being added. I.e. if something named cos already exists in /* Enclosing */, it's not a re-declaration. This is by design, because using directives can potentially bring a lot of names in, and so it shouldn't cause an error when the names they bring are not actually used.

In your case however, the name brought in is used.

When /* Enclosing */ is Base, it matches only one declaration, the one in Sibling1, as-if it was declared in Base.

When /* Enclosing */ is the global namespace, it matches the actual declaration there too, presumably the one brought in by math.h (you seem to be using that). So you get an ambiguity (the name refers to two potential entities).

So on the whole, compilers that reject this code are behaving as expected. While I understand your plight, I don't think there's really a problem for you to solve here. If client code finds Base::Sibling1::cos too verbose, it itself can employ a using directive of the form using namespace Base;. Or using a namespace alias to shorten things namespace sib1 = Base::Sibling1;.

Related