Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
Problems with Rule 14–6–2
#1
Hi all,

The second example in 14–6–2 is:

Code:
template  
void f ( T const & t )
{
   t == t;                       // Non-compliant - Calls NS::operator==
                                 // declared after f
   ::operator ==( t, t );        // Compliant - Calls built-in operator==
   ( operator ==  ) ( t, t ); // Compliant - Calls built-in operator==
}

namespace NS
{
   struct A
   {
      operator int32_t ( ) const;
   };
   bool operator== ( A const &, A const & );
}

int main ( )
{
   NS::A a;
   f ( a );
}
One problem is that the expression '::operator==(t,t)' renders the program ill-formed at template definition time (which means that, if your compiler conforms to ISO C++ 2003, it will issue an error at template definition time and refuse to generate code). This is given by ISO C++ in 14.6 temp.res, which contains this bit of normative text:
Quote:If a name does not depend on a template-parameter (as defined in 14.6.2), a declaration (or set of declarations) for that name shall be in scope at the point where the name appears in the template definition; the name is bound to the declaration (or declarations) found at that point and this binding is not affected by declarations that are visible at the point of instantiation.
Because of the way 'operator==' is written, we know that it is not a dependent name. So unqualified name lookup searches for 'operator==' and finds nothing because there is no declaration of that operator before f (either in this example or in the previous example). So the program is ill-formed (diagnostic required).

Likewise, 'operator==' would also render the program ill-formed (if it were not ill-formed already) because there is no operator== template in scope, so the '
<r>James Widman<br/>
-- <br/>
Gimpel Software<br/>
<URL url="http://gimpel.com">http://gimpel.com</URL></r>
Reply
#2
Firstly, the example is incorrect. Thank you for highlighting this.

The key motivation for this rule comes from a proposal to C++ 0X to "Fix ADL". (http://www.open-std.org/jtc1/sc22/wg21/d.../n1893.pdf)

Unfortunately C++ today does not have the recommended restrictions to ADL, and so the only way to ensure that none of the problems outlined here can occur is by disabling it completely.

One of the more interesting examples is the following:

// Example 2.3
//
#include
namespace N {
struct X { };
template
int* operator+( T , unsigned )
{ static int i ; return &i ; /* just to stub in the function body */ }
}

However, please read the complete paper for a description on why ADL in its current form is of concern.


Regards,

Richard
<r>-- <br/>
Richard Corden<br/>
Programming Research Ltd.<br/>
<EMAIL email="[email protected]">[email protected]</EMAIL><br/>
+ 44 845 0048478</r>
Reply
#3
Hi Richard,

richard_corden Wrote:The key motivation for this rule comes from a proposal to C++ 0X to "Fix ADL". (http://www.open-std.org/jtc1/sc22/wg21/d.../n1893.pdf)

Unfortunately C++ today does not have the recommended restrictions to ADL, and so the only way to ensure that none of the problems outlined here can occur is by disabling it completely.

One of the more interesting examples is [ Example 2.3]

[ Note, the latest version of N1893 is currently N2103.]

Of course, example 2.3 is caught by adhering to rule 14–5–1.

However, examples 2.1 and 2.2 (in N2103) definitely need to be discussed, because neither 14–5–1 nor 14–6–2 give us a reason to call them "non-compliant", and in both examples, ADL plays a role in undesired names being found and selected (though it's not clear to me that ADL itself is necessarily at fault).

So let's look at Example 2.1: Things go wrong because the user wanted ::user::copy() to be selected in the call within ::user::g. But both ::std::copy and ::user::copy were considered because the type of x is composed of entities from different namespaces---namely, the template ::std::vector and the class ::user::Customer. As a result, both ::std and ::user became "associated namespaces", so they were both searched during ADL for "g".

Now let's look at Example 2.2: Again, things fall apart because we have two functions, ::std::tr1::tie and ::user::tie, that both compete during overload resolution when the user only expected ::user::tie to compete.

I'd like to introduce a new term:
Quote:A namespace X is said to be related to a namespace Y if and only if X directly or indirectly encloses Y or vice versa.
Example:
Code:
namespace N1 { namespace N2 {} }
namespace G {}
// N1 is related to N2 (and vice versa).
// N1 is not related to G.
// Both N1 and G are related to the global namespace.

In example 2.2, the fact that ::std::tr1::tie was found by ADL and ::user::tie was found by unqualified lookup seems like a distraction. The important point is:
  • they're both viable for the call arguments and
  • they come from *unrelated* namespaces.
That seems to be the true recipe for disaster.

So forget about ADL; we could concoct an equally dangerous (but MISRA C++-compliant!) scenario with a pair of using-declarations that should not be simultaneously visible. (And, as 2.1 and 2.2 demonstrate, the danger is not limited to call sites within a template instantiation.)

On the other hand, we can have completely safe and well-understood (possibly-templated) code that uses ADL (possibly at instantiation time) to find a function that is logically part of the interface of a given class (as is the case in both of the examples in 14–7–2, on the lines that are currently---and undeservedly---marked "non-compliant").

So I propose a two-part change: First, delete rule 14–7–2 (because it attacks legitimate and safe class interfaces without dealing with the root of the problem discussed in N2103). Second, introduce a new rule (which probably belongs in the "clause 13" section):
Quote:During overload resolution, the set of viable candidates shall not contain a pair of candidates that were declared in unrelated namespaces.

For the special case of built-in operators, I suspect they should be regarded as being in some sense "global", because they are usually in the initial overload set even if they do not become viable candidates. Therefore we should regard them as belonging to the global namespace for the purpose of this new rule (even though they don't really live there---or in any scope). So it follows that both of the ADL-using examples in rule 14–6–2, (b(t)) and (t==t), should be regarded as "compliant".

Now, I'm not totally sure whether the above formulation is quite right; for example, I could imagine a case where two candidates are not from "related" namespaces as defined above, but are instead from namespaces that are both related to a common namespace that is immediately enclosed by the global namespace (E.g., ::X::A::foo(int) and ::X::B::foo(char)), and which are intended to compete during overload resolution by the authors of ::X. But the new rule at least captures a lot of problematic cases and brings us much closer to what we really want.
<r>James Widman<br/>
-- <br/>
Gimpel Software<br/>
<URL url="http://gimpel.com">http://gimpel.com</URL></r>
Reply
#4
James Widman Wrote:During overload resolution, the set of viable candidates shall not contain a pair of candidates that were declared in unrelated namespaces.
FYI, this is too strict for ordinary C++ programming---e.g., if a class X has an implicit conversion to a primitive type or to std::string and you also give X an overloaded operator
<r>James Widman<br/>
-- <br/>
Gimpel Software<br/>
<URL url="http://gimpel.com">http://gimpel.com</URL></r>
Reply
#5
A Technical Corrigendum will correct this as:

The following forward template declaration for operator== will be added to the top of the example:

template
bool operator==(T const &, T const &); // #1

Comments need changing to say // Compliant - calls ::operator== #1, above
Posted by and on behalf of
the MISRA C++ Working Group
Reply


Forum Jump:


Users browsing this thread: 3 Guest(s)