C++ Core Guidelines: Surprises with Argument-Dependent Lookup

There is, in particular, one rule left to template interfaces which is quite interesting: T.47: Avoid highly visible unconstrained templates with common names. Admittedly, the rule T47 is often the reason for unexpected behaviour because the wrong function is called.

 

cat 633081 1280

 

Although I write today mainly about the rule T.47, I have more to say.

The get the point of the rule T.47, I have to make a short detour. This detour is about argument-dependent lookup (ADL) also known as koenig lookup named after Andrew Koenig. First of all. What is argument-dependent lookup?

Argument-Dependent Lookup (ADL)

Here is the definition of ADL:

  • Argument-dependent lookup is a set of rules for the lookup of unqualified function names. Unqualified functions names are additionally looked up in the namespace of their arguments.

Unqualified function names mean functions without the scope operator (::).. Is argument-dependent lookup bad? Off course not, ADL makes our life as a programmer easier. Here is an example.

 

#include <iostream>

int main(){
    std::cout << "Argument-dependent lookup";  
}	

 

Fine. Let me remove the syntactic sugar of operator overloading and use the function call directly.

 

#include <iostream>

int main(){
    operator<<(std::cout, "Argument-dependent lookup");
}	

This equivalent program shows what is happening under the hood. The function operator<< is called with the two arguments std::cout and a C-string "Argument-dependent lookup".

Fine? No? The question arises: Where is the definition of the function operator<<. Of course, there is no definition in the global namespace. operator<< is an unqualified function name; therefore, argument-dependent lookup kicks in. The function name is additionally looked up in the namespace of their arguments.  In this particular case the namespace std is due to the first argument std::cout considered and the lookup find the appropriate candidate: std::operator<<(std::ostream&, const char*). Often ADL provides you precisely the function you are looking for but sometimes ... . 

Now, it is the right time to write about rule T.47:

T.47: Avoid highly visible unconstrained templates with common names

In the expression std::cout << "Argument-dependent lookup", the overloaded output operator <<  is the highly visible common name because it is defined in the namespace std. The following program, based on the program of the core guidelines, shows the crucial point of this rule.

 

// argumentDependentLookup.cpp

#include <iostream>
#include <vector>

namespace Bad{
    
    struct Number{ 
        int m; 
    };
    
    template<typename T1, typename T2> // generic equality  (5)
    bool operator==(T1, T2){ 
        return false;  
    }
    
}

namespace Util{
    
    bool operator==(int, Bad::Number){   // equality to int (4)
        return true; 
    } 

    void compareSize(){
        Bad::Number badNumber{5};                            // (1)
        std::vector<int> vec{1, 2, 3, 4, 5};
        
        std::cout << std::boolalpha << std::endl;
        
        std::cout << "5 == badNumber: " <<                    
                     (5 == badNumber) << std::endl;          // (2)         
        std::cout << "vec.size() == badNumber: " << 
                     (vec.size() == badNumber) << std::endl; // (3)
        
        std::cout << std::endl;
    }
}

int main(){
   
   Util::compareSize();

}

I expect that in both cases (2 and 3) the overloaded operator == in Line (4) is called because it takes an argument of type Bad::Number (1); therefore I should get two times true.

 argumentDependentLookup

What happened here? The call in line (3) is resolved by the generic equality operator in line (5)? The reason for my surprise is that vec.size() returns a value of type std::size_type which is an unsigned integer type. This means that equality operator requires in line (4) a conversation to int. This is not necessary for the generic equality in line (5) because this is a fit without conversion. Thanks to argument-dependent lookup, the generic equality operator belongs to the set of possible overloads.

The rules states "Avoid highly visible unconstrained templates with common names". Let me see what would happen if I follow the rule and disable the generic equality operator. Here is the fixed code.

 

// argumentDependentLookupResolved.cpp

#include <iostream>
#include <vector>

namespace Bad{
    
    struct Number{ 
        int m; 
    };
    
}

namespace Util{
    
    bool operator==(int, Bad::Number){   // compare to int (4)
        return true; 
    } 

    void compareSize(){
        Bad::Number badNumber{5};                            // (1)
        std::vector<int> vec{1, 2, 3, 4, 5};
        
        std::cout << std::boolalpha << std::endl;
        
        std::cout << "5 == badNumber: " <<                    
                     (5 == badNumber) << std::endl;          // (2)         
        std::cout << "vec.size() == badNumber: " << 
                     (vec.size() == badNumber) << std::endl; // (3)
        
        std::cout << std::endl;
    }
}

int main(){
   
   Util::compareSize();

}

 

Now, the result matches my expectations.

argumentDependentLookupResolved

 Here are my remarks to the last two rules for template interfaces.

T.48: If your compiler does not support concepts, fake them with enable_if

Honestly, when I present std::enable_if in my seminars a few participants are slightly scared. Here is the simplified version of a generic greatest common divisor algorithmn.

// enable_if.cpp

#include <iostream>
#include <type_traits>

template<typename T,                                       // (1)
         typename std::enable_if<std::is_integral<T>::value, T>::type= 0>       
T gcd(T a, T b){
    if( b == 0 ){ return a; }
    else{
        return gcd(b, a % b);                              // (2)
    }
}

int main(){

    std::cout << std::endl;
                                                           // (3)
    std::cout << "gcd(100, 10)= " <<  gcd(100, 10)  << std::endl;
    std::cout << "gcd(3.5, 4)= " << gcd(3.5, 4.0) << std::endl;     

    std::cout << std::endl;

}

 

The algorithm is way to generic. It should only work for integral types. Now, std::enable_if from the type-traits library in line (1) comes to my rescue.

The expression std::is_integral (line 2) is key for the understanding of the program. This line determines whether the type parameter T is integral. If T is not integral and therefore the return value false, there will be no template instantiations for this specific type.

Only if std::is_integral returns true std::enable_if has a public member typedef type. If not line (1) is not valid. But this is not an error. 

The C++ standard says: When substituting the deduced type for the template parameter fails, the specialisation is discarded from the overload set instead of causing a compile error. There is a shorter acronym for this rule SFINAE (Substitution Failure Is Not An Error).

The output of the compilation (enable_if.cpp: 20:49) shows it. There is no template specialisation for the type double available. 

enable if

But the output shows more. (enable_if.cpp:7:71): "no named `type* in struct std::enable_if<false, double>".

T.49: Where possible, avoid type-erasure

Strange, I wrote two posts to type-erasure (C++ Core Guidelines: Type Erasure and C++ Core Guidelines: Type Erasure with Templates) and explained this quite challenging technique. Now, I should avoid it, when possible.

What's next?

With my next post, I jump from the interfaces of templates to their definition.

 

 

 

 

Thanks a lot to my Patreon Supporters: Eric Pederson, Paul Baxter,  Meeting C++, Matt Braun, Avi Lachmish, Roman Postanciuc, Venkata Ramesh Gudpati, Tobias Zindl, Dilettant, Marko, Ramesh Jangama, and Emyr Williams.

 

Thanks in particular to:  TakeUpCode 450 60

 

Get your e-book at Leanpub:

The C++ Standard Library

 

Concurrency With Modern C++

 

Get Both as one Bundle

cover   ConcurrencyCoverFrame   bundle
With C++11, C++14, and C++17 we got a lot of new C++ libraries. In addition, the existing ones are greatly improved. The key idea of my book is to give you the necessary information to the current C++ libraries in about 200 pages.  

C++11 is the first C++ standard that deals with concurrency. The story goes on with C++17 and will continue with C++20.

I'll give you a detailed insight in the current and the upcoming concurrency in C++. This insight includes the theory and a lot of practice with more the 100 source files.

 

Get my books "The C++ Standard Library" (including C++17) and "Concurrency with Modern C++" in a bundle.

In sum, you get more than 600 pages full of modern C++ and more than 100 source files presenting concurrency in practice.

 

Get your interactive course

 

Modern C++ Concurrency in Practice

C++ Standard Library including C++14 & C++17

educative CLibrary

Based on my book "Concurrency with Modern C++" educative.io created an interactive course.

What's Inside?

  • 140 lessons
  • 110 code playgrounds => Runs in the browser
  • 78 code snippets
  • 55 illustrations

Based on my book "The C++ Standard Library" educative.io created an interactive course.

What's Inside?

  • 149 lessons
  • 111 code playgrounds => Runs in the browser
  • 164 code snippets
  • 25 illustrations

Add comment


Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 1381

All 1407569

Currently are 167 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments