Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using "using" twice interpreted differently by different compilers

Tags:

c++

c++11

Consider the following code:

struct A {
    int propose();
};

struct A1 : A {
    int propose(int);
    using A::propose;
};

struct B1 : A1 {
protected:
    using A1::propose;
public:
    using A::propose;
};

int main() {
    B1().propose();
}

Let's compile this: g++ -std=c++11 main.cpp.

I'm getting the following compiler error using GNU 4.8.1:

main.cpp: In function 'int main()':                                                                                                                    
main.cpp:2:9: error: 'int A::propose()' is inaccessible
     int propose();
         ^
main.cpp:18:18: error: within this context
     B1().propose();

However, this code compiles in AppleClang 6.0.0.6000056.

I understand that there is no need for the using in B1, (in my code was necessary, but I had 1 using too much by mistake). In any case, why Clang compiles it? Is this expected?

like image 341
Jorge Leitao Avatar asked Jul 10 '15 14:07

Jorge Leitao


2 Answers

In [namespace.udecl], we have:

When a using-declaration brings names from a base class into a derived class scope, member functions and member function templates in the derived class override and/or hide member functions and member function templates with the same name, parameter-type-list (8.3.5), cv-qualification, and ref-qualifier (if any) in a base class (rather than conflicting).

The standard explicitly says that names brought in will not conflict with names in a base class. But it doesn't say anything about bringing in conflicting names.

The section also says:

A using-declaration is a declaration and can therefore be used repeatedly where (and only where) multiple declarations are allowed. [ Example:

struct B {
    int i;
};

struct X : B {
    using B::i;
    using B::i; // error: double member declaration
};

—end example ]

And interestingly, in the following example it's GCC that happily compiles it (and prints A) while Clang allows the construction of a C but rejects the call to foo as ambiguous:

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

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

struct C : A, B {
    using A::foo;
    using B::foo;
};


int main()
{
    C{}.foo();
    return 0;
}

So the short answer is - I suspect this is underspecified in the standard and that both compilers are doing acceptable things. I would just avoid writing this sort of code for general sanity.

like image 76
Barry Avatar answered Oct 24 '22 12:10

Barry


The declaration is legal.

Calling it is legal and should work anywhere, and it can only be called from the class and derived classes, and it can be called from within any class. You'll note that this makes little sense.

There are no rules that ban that construct in declarations (importing the name twice from two different base classes with the same signature), and it is even used in "real" code where the derived class goes and hides the name after they are imported.

If you don't hide it, you are in the strange situation where the same function A::propose is both protected and public at the same time, as it is named twice (legally) in the same scope with different access control. This is ... unusual.

If you are within a class, a sub-clause says you can use it:

[class.access.base]/5.1

A member m is accessible at the point R when named in class N if — (5.1) m as a member of N is public

and propose is clearly public. (it is also protected but we don't have to keep reading for that case!)

Elsewhere, we have a contradiction. You are told you can use it everywhere without restriction [class.access]/1(3). And you are told that you can only use it in certain circumstances [class.access]/1(2).

I am uncertain how to resolve that ambiguity.


The rest of the logic train:

In [namespace.udecl]/10 we have:

A using-declaration is a declaration and can therefore be used repeatedly where (and only where) multiple declarations are allowed.

And [namespace.udecl]/13:

Since a using-declaration is a declaration, the restrictions on declarations of the same name in the same declarative region

so each of those using X::propose; are declarations.

[basic.scope] has no applicable restrictions on two functions of the same name in a scope, other than [basic.scope.class]/1(3) which states that if reordering of declarations changes the program, the program is ill-formed. So we cannot say that the later one wins.

Two declarations of member functions in the same scope are legal under [basic.scope]. However, under [over], there are restrictions on two member functions with the same name.

[over]/1 states:

When two or more different declarations are specified for a single name in the same scope, that name is said to be overloaded

And there are some restrictions on overloading. This is what usually prevents

struct foo {
  int method();
  int method();
};

from being legal. However:

[over.load]/1 states:

Not all function declarations can be overloaded. Those that cannot be overloaded are specified here. A program is ill-formed if it contains two such non-overloadable declarations in the same scope. [Note: This restriction applies to explicit declarations in a scope, and between such declarations and declarations made through a using-declaration (7.3.3). It does not apply to sets of functions fabricated as a result of name lookup (e.g., because of using-directives) or overload resolution (e.g., for operator functions). —end note

the note explicitly permits symbols introduced via two using-declarations from being considered by the overloading restrictions! The rules only apply to two explicit declarations within the scope, or between an explicit declaration within the scope and a using declaration.

There are zero restrictions on two using-declarations. They can have the same name, and their signatures can conflict as much as you'd like.

This is useful, because usually you can go and then hide their declaration (with a declaration in the derived class), and nothing goes wrong [namespace.udecl]/15:

When a using-declaration brings names from a base class into a derived class scope, member functions and member function templates in the derived class override and/or hide member functions and member function templates with the same name, parameter-type-list (8.3.5), cv-qualification, and ref-qualifier (if any) in a base class (rather than conflicting).

However, this is not done here. We then call the method. Overload resolution occurs.

See [namespace.udecl]/16:

For the purpose of overload resolution, the functions which are introduced by a using-declaration into a derived class will be treated as though they were members of the derived class. In particular, the implicit this parameter shall be treated as if it were a pointer to the derived class rather than to the base class. This has no effect on the type of the function, and in all other respects the function remains a member of the base class.

So we have to treat them as if they are members of the derived class for the purpose of overload resolution. But there are still 3 declarations here:

protected:
  int A::propose(); // X
  int A1::propose(int); // Y
public:
  int A::propose(); // Z

Thus the call to B1().propose() considers all 3 declarations. Both X and Z are equal. They, however, refer to the same function, and overload resolution states there is an ambiguity if two different functions are selected. So the result is not ambiguous. There may be access control violations, or not, depending on how you read the standard.

[over.match]/3

If a best viable function exists and is unique, overload resolution succeeds and produces it as the result. Otherwise overload resolution fails and the invocation is ill-formed. When overload resolution succeeds, and the best viable function is not accessible (Clause 11) in the context in which it is used, the program is ill-formed.

like image 2
Yakk - Adam Nevraumont Avatar answered Oct 24 '22 14:10

Yakk - Adam Nevraumont