cat 633081 1280

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

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

 

cat 633081 1280

 

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

The get to the point of 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 an argument-dependent lookup?

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

Be part of my mentoring programs:

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (starts March 2024)
  • Do you want to stay informed: Subscribe.

     

    Argument-Dependent Lookup (ADL)

    Here is the definition of ADL:

    • Argument-dependent lookup is a set of rules for looking up unqualified function names. Unqualified function 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? Of 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 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 finds the appropriate candidate: std::operator<<(std::ostream&, const char*). Often ADL provides you precisely with 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 obvious 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 the equality operator requires a conversation to int in line (4). This is unnecessary 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 rule states “Avoid highly visible unconstrained templates with common names”. Let me see what would happen if I followed 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 on the last two rules for template interfaces.

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

    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 algorithm.

    // 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 too 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 critical for understanding 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. Suppose 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 specialization 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 compilation output (enable_if.cpp: 20:49) shows it. There is no template specialization 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: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Dmitry Farberov, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, moon, Philipp Lenk, Hobsbawm, and Charles-Jianye Chen.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Seminars

    I’m happy to give online seminars or face-to-face seminars worldwide. Please call me if you have any questions.

    Standard Seminars (English/German)

    Here is a compilation of my standard seminars. These seminars are only meant to give you a first orientation.

    • C++ – The Core Language
    • C++ – The Standard Library
    • C++ – Compact
    • C++11 and C++14
    • Concurrency with Modern C++
    • Design Pattern and Architectural Pattern with C++
    • Embedded Programming with Modern C++
    • Generic Programming (Templates) with C++
    • Clean Code with Modern C++
    • C++20

    Online Seminars (German)

    Contact Me

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *