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

Contents[Show]

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 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 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: Matt Braun, Roman Postanciuc, Tobias Zindl, Marko, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Darshan Mody, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Animus24, Jozo Leko, John Breland, espkk, Wolfgang Gärtner,  Louis St-Amour, Stephan Roslen, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Avi Kohn, Robert Blanch, Truels Wissneth, Kris Kafka, Mario Luoni, Neil Wang, Friedrich Huber, Sudhakar Balagurusamy, lennonli, and Pramod Tikare Muralidhara.

 

Thanks in particular to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, and Dendi Suhubdy

 

Seminars

I'm happy to give online-seminars or face-to-face seminars world-wide. Please call me if you have any questions.

Bookable (Online)

Deutsch

English

Standard Seminars 

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

New

Contact Me

Modernes C++,

RainerGrimmSmall

 

Tags: templates

My Newest E-Books

Course: Modern C++ Concurrency in Practice

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

Course: Embedded Programming with Modern C++

Course: Generic Programming (Templates)

Course: C++ Fundamentals for Professionals

Subscribe to the newsletter (+ pdf bundle)

Blog archive

Source Code

Visitors

Today 8136

Yesterday 6384

Week 8136

Month 166567

All 5035881

Currently are 124 guests and no members online

Kubik-Rubik Joomla! Extensions

Latest comments