C++ - Argument Dependent Lookup
C++ 언어가 가진 화려한 기구의 하나로 ADL(Argument Dependent Lookup)라는 것이 있다.
번역하면 “실 매개 변수 의존 이름 검색”으로 읽고, 글씨처럼 “실 매개 변수”에 의존하는 “이름 검색”이 된다.
ADL의 동작
코드를 보면서 차례로 이해해 보자.
int main() {
func();
return 0;
}
func라는 함수가 불리는 코드이다. 이걸 그냥 컴파일 하면 에러이다.
왜냐하면……func라는 함수는 코드 상 어디에도 정의되지 않아서!!
#include <iostream>
void func() {
std::cout << "call func. yes!" << std::endl;
}
int main() {
func();
return 0;
}
이번에는 컴파일 오류가 되지 않고 무사히 func 함수를 호출한다.
#include <iostream>
namespace ns1 {
void func() {
std::cout << "call func. yes!" << std::endl;
}
} // namespace ns1
int main() {
func();
return 0;
}
func 함수를 namespace ns1로 에워쌌다.
이 코드는 다시 컴파일 오류가 된다.
9행에서 호출하고 있는 func의 심볼을 글로벌 네임 스페이스에서 찾으려고 하지만 실제로 정의되고 있는 func는 ns1 네임 스페이스 안에 있어서 통상의 이름 검색으로는 히트하지 않기 때문이다.
#include <iostream>
namespace ns1 {
void func() {
std::cout << "call func. yes!" << std::endl;
}
} // namespace ns1
int main() {
ns1::func(); // ns1의 func을 호출한다
return 0;
}
#include <iostream>
namespace ns1 {
struct Color {
int r = 255;
int g = 255;
int b = 0;
};
void print(Color& color) {
std::cout << color.r << std::endl;
std::cout << color.g << std::endl;
std::cout << color.b << std::endl;
}
} // namespace ns1
int main() {
ns1::Color c;
print(c); // print 에 ns1을 붙이지 않고 호출. 타입 미스는 아니다
return 0;
}
이 main 함수를 잘 보자.
Color의 타입에는 ns1::
라는 지정이 붙어 있는데 print는 네임 스페이스 지정을 부여하지 않고 호출한다.
그러나 이 코드는 컴파일 오류가 아니다.
왜냐하면 C++은 어떤 타입의 오브젝트들이 함수 호출 시 실제 인수로서 이용되면 관련된 네임 스페이스에서도 그 함수가 탐색된다는 사양이기 때문이다.
이번 경우는 print의 인수로 전달되는 c의 타입이 ns1::Color
로 ns1
네임 스페이스에 관련이 있는 만큼 print라는 함수의 심볼 검색 글로벌 네임 스페이스에서 뿐만 아니라 관계가 있는 ns1에서도 검색하겠다는 동작이 된다.
이것이 “실제 매개 변수 의존 이름 검색(ADL)”의 동작이다.
그럼 “관련된 네임 스페이스”는 무엇일까?
인수가 클래스 구성원이었을 경우
그 클래스 자신 및 기저 클래스, 그리고 그 클래스를 둘러싼 네임 스페이스이다.
예를 들면 std::vector::iterator는 클래스 멤버이므로 vector와 std가 관련된 네임 스페이스가 된다.
인수가 네임 스페이스의 멤버인 경우
어떤 네임 스페이스 안에서 정의되고 있는 클래스의 경우 관련하는 네임 스페이스는 그 클래스를 둘러싼 네임 스페이스이다. 그 클래스가 계승하고 있으면 계승 전 클래스에 대해서 같은 조건에 의거하여 관련하는 네임 스페이스가 결정된다
주의점
전술대로 ADL은 인간의 눈으로 언뜻 보아서는 모르는 네임 스페이스에서도 심볼을 찾으려 한다.
그 결과 본래 의도하지 않는 함수가 호출될 위험성도 있다.
그래도 괜찮다 예기치 않은 ADL의 방지책도 잘 있다.
ADL Firewall
왜 존재하는가
복잡성을 더할 뿐인 이 ADL 이라는 구조가 왜 필요한가.
그것은 연산자를 네임 스페이스 수식 없이 부를 수 있게 하기 위해서이다.
#include<cstdio>
namespace ns2{
struct Money{
Money(int v) : value(v){}
int value;
};
//Money 용 2항 + 연산자
Money operator+(const Money&lhs, const Money&rhs){
return Money(lhs.value+rhs.value);
}
}//namespace ns2
int main() {
ns2::Money a(5), b(1);
ns2::Money c=a+b; //만약 ADL이 없다면 2항 연산자+ 를 이렇게 부를 수 없다
return 0;
}
ADL이라는 구조가 없다면
ns2::Money c = a + b;
ns2::Money c = ns2::operator+(a, b);
라는 그다지 이쁘지 않은 호출 밖에 할 수 없게된다.
이는 스트림 연산자 포함한 모든 연산자에게도 해당된다.
자주 등장하는 « 연산자도 본래 std의 네임 스페이스 안에 있으므로 ADL이 없다면 직감적으로 호출 할 수 없다.
#include <iostream>
#include <string>
int main() {
std::string one_more_thing = "8K iMac coming soon";
std::cout << one_more_thing << std::endl; // std::<<의 호출
}
namespace boost<
//Private copy constructor and copy assignment ensure classes derived from
//class noncopyable cannot be copied.
//Contributed by Dave Abrahams
namespace noncopyable_//protection from unintended ADL
{
class noncopyable
{
protected:
noncopyable(){}
~noncopyable(){}
private://emphasize the following members are private
noncopyable(const noncopyable&);
const noncopyable&operator=(const noncopyable&);
};
}
typedef noncopyable_;noncopyable noncopyable;
}//namespace
8행째의 주석이 흥미롭다.
protection from unintended ADL(뜻밖의 ADL의 방지)
주지하다시피 ADL에서는 뜻밖의 이름 공간까지 검색 대상이 될 수 있으며, 때로는 불행한 오류의 온상이 될 수 있다.
그리고 그것은, ADL로 검색되는 네임 스페이스에 실제 매개 변수의 이름 공간이 암묵적으로 포함되기 때문인데 이 boost의 코드에서는 그 예방책이 강구되고 있다는 것이다.
21행째에
typedef noncopyable_::noncopyable noncopyable;
가 있다.
이 일행의 typedef로 본래 boost::noncpyable_::noncopyable라는 접근을 boost::noncopyable라고 쓸 수 있게 되었다.
그러나 실제 noncopyable은 noncopyable_라는 네임 스페이스에 존재하고 있어서, ADL에 의해서 boost 네임 스페이스 모두가 검색 후보가 되는 것을 막고 있다.
이것이 ADL Firewall이라는 방법이다.
이렇게 중첩된 네임 스페이스에 타입을 가두고, 네임 스페이스별 에일리어싱을 붙임으로써 접근하기 쉽게 하는 것으로, inline namespace에서는 ADL 대책이 안 되므로 조심하자.
inline namespace는 ADL 대책에는 안 된다!
이 글은 2020-08-05에 작성되었습니다.