How To: Argument Dependent Lookup in C++ and Templates

Introduction

This post aims to provide some hidden quirks on using the keyword friend in Cpp templates. I will be referencing and unraveling some of the text from the book “C++ Templates: The Complete Guide”. It’s quite an amazing read. Recommend you to get your hands on it to level up your C++ game.
Before jumping into using friend function, I want to discuss about the Argument Dependent Lookup in C++ (ADL) with respect to templates and how compilers tackle some of the issues that templates poses.

What is ADL?

To get a sense of what Argument-Dependent Lookup is, let’s take a look at following snippet (paraphrased from the book)

// My Templated Max() function
template<typename T>
const T& Max(const T& a, const T& b)
{
	return a < b ? b : a;
}
// A class in new namespsace
namespace BigStuff
{
	class BigNum
	{
	public:
		BigNum(int pNum)
		: num(pNum)
		{}
		int num;
	};
	bool operator<(const BigNum& a, const BigNum& b)
	{
		return a.num < b.num;
	}
}
// Usage
int main()
{
	BigStuff::BigNum bignum1(10);
	BigStuff::BigNum bignum2(10);

	std::cout << "Namespace Test1: " << (Max(bignum1, bignum2)).num;
}

In the snippet above, I have a templated Max() function that takes two arguments of generic type.
Next, I declared a new namespace “BigStuff” with a class “BigNum”. This namespace also has an overridden ‘<‘ operator to handle comparison against objects of type “BigNum”
Finally, in main(), we create two objects of type BigStuff:BigNum and call our templated Max() on them.

Now, here’s the question. Going by the usual rules, how does the compiler know the existence of the overridden ‘<‘ operator for BigNum types?
If we go with simple Unqualified-Name Lookup, it only checks the current and subsequent parent namespaces and wouldn’t be able to find the required overridden operator. Here’s where ADL comes to save the day.

If the name is unqualified (not preceded by :: or any other name, e.g., Base::<name> ), the scope of arguments is also considered for the Lookup.
In our case, the scope of “BigNum”(class scope) is checked. Since there’s no operator found, it again searches for the enclosing namespace “BigStuff” which contains our overridden ‘<‘ operator. Since it found a matching name, no further scopes are examined and the lookup stops here.
That is reason we are auto-magically able to access something defined in a different scope from where the function is called.

Back to the Original Topic

Now that we have enough context at hand, lets examine another snippet and try to analyze the behavior of the code:

namespace Injection
{
	template<typename T>
	class FriendInj;

	void injFuncNoparam()
	{
		std::cout << "Inside friendInjection No Param function\n";
	}

	void injFuncExternal(const FriendInj<int>& t)
	{
		std::cout << "Inside friend injection function\n";
	}

	template<typename T>
	class FriendInj
	{
	public:
		friend void injFunc()
		{
			std::cout << "Internal injected functon. No Params\n";
		}
	
		friend void injFuncNoparam();
		friend void injFuncExternal(const FriendInj<T>&);
		friend void injFuncInternal(const FriendInj<T>&)
		{
			std::cout << "Inernal injected function\n";
		}

		int a = 5;
		int b = 10;
	};
}

Here’s what we have in the code above:

  • injFuncNoparam in namespace Injection that has no parameters
  • injFuncExternal in namespace Injection that takes Injection::FriendInj& as a parameter
  • Templated class FriendInj with 3 friend functions.
    injFuncInternal is interesting as it’s declared and defined within the class, but it’s not a member function of FriendInj.

Now that we have established the components, lets try to use the friend functions. The code in the snippet below is written in global namespace.

//Usage
void CallFunc()
{
	const Injection::FriendInj<int> inj;

	//injFuncNoparam(); // Cannot find the defined-friend function. Throws an error
	injFuncInternal(inj); // Visible due to ADL
	injFuncExternal(inj); // Visible due to ADL

	//injFunc(); // Cannot find. So, throws an error
}

int main()
{
	CallFunc();
    return 0;
}

It is valid to call injFuncInternal or injFuncExternal (declared and defined in namespace Injection) by passing the proper object as an argument.
BUT, when trying to access injFuncNoparam (again, declared and defined in namespace Injection), the compiler throws an error.
In the first place, it’s strange that we are able to find injFuncInternal and injFuncInternal defined in a different scope, without any additional syntax.

This bizarre behavior is thanks to Argument-Dependent lookup.
The function names injFuncInternal and injFuncInternal are “Unqualified-Names” and they fulfill the requirements to perform ADL. Since we are passing object of type Injection::FriendInj, the scopes of class FriendInj and Injection are lookup to find the required functions.
We could remove friend keyword on injFuncInternal and still find the class, but removing friend on injFuncInternal will throw an error as it becomes a regular member function in this case.
Defining a friend function in a class injects that function in the class’s scope!! So, it’s valid to call it as follows:
Injection::FriendInj::injFuncInternal(t);

Conclusion

That is all I’ve got or this post. Please let me know if something not right and hope that this is informative.
Do visit again!


Leave a Reply

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