DEV Community

Coral Kashri
Coral Kashri

Posted on • Originally published at cppsenioreas.wordpress.com on

ADL – Avoid Debugging Later

Back in the day, being a witch was considered a grave crime. Today, we’re diving into one of C++’s lesser-known spells: ADL (Argument-Dependent Lookup). But before we explore this arcane magic, you must heed a warning—black magic comes with consequences. ADL is particularly treacherous, often leading to frustrating and hard-to-debug issues. Whenever possible, it’s wise to avoid casting this spell unless absolutely necessary.

Ingredients

Every spell needs ingredients, this time the only ingredients you need are a C++ compiler and a function that accepts at least one parameter, but there is a try catch, the parameter type has to belong to the same namespace of the function.

Casting the Spell

This spell works in shadows—you must look closely to uncover its effect.


std::cout << "Can you see me?";

Enter fullscreen mode Exit fullscreen mode

Should the spell have passed you by, I’ll summon its power again for your eyes:


std::vector<int> a{1}, b{2};
swap(a, b);

Enter fullscreen mode Exit fullscreen mode

If the spell’s effect remains elusive, let’s summon the entire code for you to see:


#include <vector>

int main() {
    std::vector<int> a{1}, b{2};
    swap(a, b);
    return 0;
}

Enter fullscreen mode Exit fullscreen mode

And there it is, I didn’t use using namespace std; and yet I called std::swap function without the std:: prefix.

Dangerous Path

Before we dive into unveiling the syntax secrets, this is the place for warning signs. We all know that using namespace std; is considered bad practice—and for a good reason. When you import an entire namespace into your own without being fully aware of the functions and names you’re bringing in, collisions can occur. Such collisions might result in calling an unintended function or class, often one with a slightly different algorithm or mechanism (after all, there’s usually a reason for the identical naming).

Calling an unexpected function might unintentionally summon dependencies from distant, unforeseen corners of your code, and in doing so, awaken Undefined Behavior lurking quietly in the shadows, waiting for the right moment to strike.

This ‘spell’ exposes you to the exact same issue, even without explicitly using using namespace. There are situations where you may need to cast this spell to achieve generic or scalable code. However, even in those cases, you must ensure the area is clear. Treat these spell locations as dangerous and suspicious zones —mark them clearly, and suspect them first whenever a bug appears nearby.

Syntax Ritual: Unveiling the Dark Art of ADL

This spell was originally crafted to support the well-known foundation of C++ code:


std::cout << "Hello World";

Enter fullscreen mode Exit fullscreen mode

This simple, foundational code uses ADL to access std::operator<<(std::ostream&, const char*), as the actual code is:


std::operator<<(std::cout, "Hello World"); // Without using ADL
operator<<(std::cout, "Hello World"); // Using ADL

Enter fullscreen mode Exit fullscreen mode

The general idea behind this magic is that the compiler searches for a matching function in the current namespace and in the namespaces of each argument in the function call. For example, in the code above, the operator<< is located in the std namespace, and the argument std::cout also belongs to the same namespace.

However, there are many more rules regarding what the lookup would contain in each case. The exact rules of the ADL can be found on cppreference.

A slightly more complex example comes from my experience with the dlib library, which looks like this:


namespace dlib {
    class dense_feature_extractor {
    public:
        friend void serialize(const dense_feature_extractor& item, std::ostream& out) {
            out << "dense_feature_extractor::serialize\n";
        }

        friend void deserialize(dense_feature_extractor& item, std::istream& in) {
            std::cout << "dense_feature_extractor::deserialize\n";
        }
    };

    class matrix {};

    void serialize(const matrix& m, std::ostream& out) {
        out << "dlib::serialize(matrix)\n";
    }

    void deserialize(const matrix& m, std::istream& in) {
        std::cout << "dlib::deserialize(matrix)\n";
    }
}

Enter fullscreen mode Exit fullscreen mode

Now, it becomes interesting: without ADL, we can’t access the serialize and deserialize friend functions inside the dense_feature_extractor class, unless we modify the library to declare them outside the class or make them static functions within the class.

By casting a few ADL spells, we can craft the following function:


dlib::matrix m;
dlib::dense_feature_extractor d;
serialize(m, std::cout); // dlib::serialize(matrix)
// same as: dlib::serialize(m, std::cout);
serialize(d, std::cout);// dense_feature_extractor::serialize
deserialize(m, std::cin); // dlib::deserialize(matrix)
// same as: dlib::deserialize(m, std::cin);
deserialize(d, std::cin); // dense_feature_extractor::deserialize

Enter fullscreen mode Exit fullscreen mode

Since the calls appear identical, we can generalize them as follows:


template <typename T>
void dlib_serializer(const T &val) {
    serialize(val, std::cout);
}

template <typename T>
void dlib_deserializer(T &val) {
    deserialize(val, std::cin);
}

void func() {
    dlib::matrix m;
    dlib::dense_feature_extractor d;
    dlib_serializer(m);
    dlib_serializer(d);
    dlib_deserializer(m);
    dlib_deserializer(d);
}

Enter fullscreen mode Exit fullscreen mode

The Spell You Might Unintentionally Already Cast

This spell is subtle and elusive, and you may already be summoning it without even noticing—simply by forgetting the std:: prefix (or any other namespace’s arcane sigil). The silver lining is that now, armed with this knowledge, you’ll be able to spot its presence in any code that calls upon it.

Special Thanks

  • Ellie Bogdanov for spelling assistance.
  • ChatGPT for the logo and for grammar and spelling assistance.

Top comments (0)