C++ ADL(인자 종속 조회): swap 관용구가 동작하는 이유와 엉뚱한 함수가 호출될 때
이 글의 핵심
ADL 덕분에 operator<<나 swap을 네임스페이스 없이 자연스럽게 쓸 수 있지만, 같은 규칙 때문에 엉뚱한 오버로드가 호출되거나 템플릿 안에서 결과가 달라지기도 합니다. 중첩 네임스페이스와 빌트인 타입에서의 검색 범위, using 선언과의 충돌, 괄호로 ADL을 끄는 방법을 짚어 네임스페이스를 설계할 때의 기준을 제공합니다.
ADL이란?
ADL(Argument Dependent Lookup) 은 함수 이름을 찾을 때, 글자 그대로 인자(Argument)와 연관된 네임스페이스까지 함께 뒤지는 규칙입니다. 우편물을 보낼 때 수신인 이름만 보지 않고 주소가 속한 동·구까지 함께 찾아가는 것과 비슷합니다. 따라서 std::를 매번 붙이지 않아도 print(p)처럼 자연스럽게 쓸 수 있으며, operator<<를 타입과 같은 네임스페이스에 두는 관용이 성립합니다.
// 패키지 선언
namespace MyLib {
struct Point {
int x, y;
};
void print(const Point& p) {
std::cout << "(" << p.x << ", " << p.y << ")" << std::endl;
}
}
int main() {
MyLib::Point p{10, 20};
// ADL: MyLib::print 자동 찾음
print(p); // MyLib:: 불필요
// 명시적 호출도 가능
MyLib::print(p);
}
main에는 print라는 이름이 보이지 않는데도 컴파일이 되는 이유는, 컴파일러가 함수 이름을 찾을 때 두 가지 검색을 함께 하기 때문입니다. 하나는 호출한 위치에서 바깥쪽 스코프로 올라가며 찾는 일반 검색(unqualified lookup)이고, 다른 하나는 인자 p의 타입 MyLib::Point가 선언된 네임스페이스를 뒤지는 ADL입니다. 두 검색에서 나온 후보를 합친 뒤 오버로드 해석으로 하나를 고릅니다. ADL은 print(p)처럼 한정되지 않은 함수 호출에만 적용되고, MyLib::print(p)처럼 네임스페이스를 붙이면 그 네임스페이스만 검색합니다.
C++ 표준이 이 규칙을 둔 가장 큰 이유는 연산자입니다. a + b는 operator+(a, b) 호출인데, 연산자 문법에는 네임스페이스를 붙일 자리가 없습니다. ADL이 없다면 std::string끼리 더하는 코드마다 using namespace std;를 써야 했을 것입니다. 참고로 위 예제는 #include <iostream>이 빠져 있으니 직접 컴파일할 때는 추가해야 합니다.
작동 원리
같은 이름 func가 전역과 다른 네임스페이스에 모두 있을 때, 인자 x의 타입이 정의된 네임스페이스 쪽 후보가 ADL로 끌려옵니다. 아래에서는 func(x)가 A::func를 고르는 이유를 확인할 수 있습니다.
namespace A {
struct X {};
void func(X) {
std::cout << "A::func" << std::endl;
}
}
namespace B {
void func(A::X) {
std::cout << "B::func" << std::endl;
}
}
int main() {
A::X x;
func(x); // ADL: A::func 호출
B::func(x); // 명시적: B::func 호출
}
B::func도 A::X를 받지만 후보가 되지 못합니다. ADL이 뒤지는 곳은 “인자 타입과 연관된 네임스페이스”이고, 함수가 그 타입을 매개변수로 받는다는 사실은 연관 관계를 만들지 않습니다. 연관 네임스페이스는 인자 타입이 선언된 네임스페이스, 클래스라면 그 기반 클래스들의 네임스페이스, 템플릿 특수화라면 템플릿 인자 타입들의 네임스페이스까지 포함합니다. 그래서 std::vector<A::X>를 인자로 넘기면 std와 A가 모두 검색됩니다. 포인터나 참조는 가리키는 타입을 기준으로 합니다.
std::cout과 ADL
operator<<(std::ostream&, const Point&)를 MyLib에 두면, std::cout << p를 쓸 때 스트림은 std에, Point는 MyLib에 있어도 ADL이 MyLib의 연산자를 찾아 줍니다. 사용자 정의 타입의 출력 연산자를 전역에 흩뿌리지 않고도 쓸 수 있는 이유입니다.
#include <iostream>
namespace MyLib {
struct Point {
int x, y;
};
// ADL로 자동 찾음
std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << "(" << p.x << ", " << p.y << ")";
}
}
int main() {
MyLib::Point p{10, 20};
// ADL: MyLib::operator<< 자동 찾음
std::cout << p << std::endl;
}
std::cout << p에서 컴파일러는 첫 인자 std::ostream에서 std를, 두 번째 인자 MyLib::Point에서 MyLib을 연관 네임스페이스로 잡습니다. std 안에는 int, const char* 등을 위한 operator<<가 수십 개 있지만 Point를 받는 것은 MyLib에 둔 것 하나뿐이라 그것이 선택됩니다.
출력 연산자를 MyLib이 아니라 전역에 정의해도 main에서 호출하는 경우에는 동작합니다. 문제는 템플릿 안에서 호출될 때입니다. std::ostream_iterator나 로깅 라이브러리처럼 std 또는 다른 네임스페이스 안의 템플릿이 os << value를 호출하면, 일반 검색은 그 템플릿이 있는 네임스페이스에서 시작해 같은 이름의 operator<<를 만나는 순간 멈추기 때문에 전역 연산자까지 올라가지 못합니다. 이때 남는 방법이 ADL뿐이므로 연산자는 타입과 같은 네임스페이스에 둬야 합니다. 전역에 둔 연산자가 main에서는 되는데 std::copy(v.begin(), v.end(), std::ostream_iterator<Point>(std::cout))에서는 “no match for ‘operator<<‘“로 실패하는 경우가 전형적인 증상입니다.
클래스 안에서 friend로 정의하는 hidden friend 방식도 널리 씁니다. struct Point { friend std::ostream& operator<<(std::ostream& os, const Point& p) { ... } };처럼 클래스 본문 안에서 정의한 friend 함수는 네임스페이스 스코프에 이름이 드러나지 않고 ADL로만 찾을 수 있습니다. 그 결과 Point와 무관한 << 호출의 후보 목록에 끼어들지 않아 오버로드 해석이 빨라지고 오류 메시지도 짧아집니다. friend 키워드에서 friend 자체의 규칙을 다룹니다.
실전 예시
예시 1: 연산자 오버로딩
namespace Math {
struct Vector2 {
float x, y;
};
Vector2 operator+(const Vector2& a, const Vector2& b) {
return {a.x + b.x, a.y + b.y};
}
Vector2 operator-(const Vector2& a, const Vector2& b) {
return {a.x - b.x, a.y - b.y};
}
Vector2 operator*(const Vector2& v, float scalar) {
return {v.x * scalar, v.y * scalar};
}
std::ostream& operator<<(std::ostream& os, const Vector2& v) {
return os << "Vector2(" << v.x << ", " << v.y << ")";
}
}
int main() {
Math::Vector2 v1{1.0f, 2.0f};
Math::Vector2 v2{3.0f, 4.0f};
// ADL: Math::operator+ 자동 찾음
auto v3 = v1 + v2;
auto v4 = v1 - v2;
auto v5 = v1 * 2.0f;
std::cout << v3 << std::endl;
std::cout << v4 << std::endl;
std::cout << v5 << std::endl;
}
v1 * 2.0f는 되지만 2.0f * v1은 컴파일되지 않는다는 점에 주의하세요. 정의한 것은 operator*(const Vector2&, float) 하나뿐이고, 연산자는 인자 순서를 바꿔 주지 않습니다. 교환 법칙이 필요하면 operator*(float, const Vector2&)도 같은 네임스페이스에 추가합니다. 이런 이항 연산자를 멤버가 아닌 자유 함수로 두는 이유도 ADL과 관련이 있는데, 멤버 연산자는 왼쪽 피연산자가 반드시 그 클래스여야 하지만 자유 함수는 양쪽 인자 모두에 암시적 변환을 적용할 수 있어 대칭적으로 동작합니다. 자세한 규칙은 연산자 오버로딩을 참고하세요.
예시 2: swap 함수
namespace MyLib {
struct BigObject {
std::vector<int> data;
BigObject(size_t size) : data(size) {}
};
// 커스텀 swap (ADL)
void swap(BigObject& a, BigObject& b) noexcept {
a.data.swap(b.data);
std::cout << "MyLib::swap 호출" << std::endl;
}
}
int main() {
MyLib::BigObject obj1(1000);
MyLib::BigObject obj2(2000);
// ADL: MyLib::swap 호출
using std::swap; // fallback
swap(obj1, obj2);
}
이 두 줄이 흔히 “two-step swap” 관용구라고 부르는 패턴입니다. using std::swap;으로 일반 검색이 std::swap을 찾게 해 두고, 한정 없이 swap(obj1, obj2)를 호출하면 ADL이 MyLib::swap을 추가로 찾습니다. 두 후보 중 std::swap은 템플릿이고 MyLib::swap은 BigObject&를 정확히 받는 일반 함수라, 오버로드 해석에서 템플릿이 아닌 쪽이 이깁니다. int처럼 사용자 정의 swap이 없는 타입이면 std::swap으로 떨어집니다.
std::swap(obj1, obj2)로 한정해서 쓰면 ADL이 꺼지므로 MyLib::swap은 무시되고, 이동 생성자와 이동 대입 세 번으로 교환하는 일반 버전이 호출됩니다. BigObject는 vector를 가지고 있어 이동도 저렴하지만, 이동 연산이 없는 오래된 클래스라면 전체 복사가 세 번 일어납니다. 반대로 using 줄을 빼면 사용자 정의 swap이 없는 타입에서 컴파일 오류가 납니다. 제네릭 코드를 쓰다 보면 이 두 줄 중 하나를 빠뜨리는 실수를 한 번쯤 하게 되는데, 컴파일은 되고 결과도 같아서 성능 저하로만 드러나기 때문에 오래 방치되기 쉽습니다.
C++20의 std::ranges::swap은 이 두 단계를 내부에서 대신 해 주는 customization point object(CPO)입니다. 함수가 아니라 객체라서 그 자체는 ADL 대상이 되지 않고, 내부에서 ADL로 사용자 정의 swap을 찾아 있으면 쓰고 없으면 이동 기반 교환을 합니다. 새 코드에서는 std::ranges::swap(a, b) 한 줄로 같은 효과를 얻을 수 있습니다.
예시 3: 비교 연산자
namespace Data {
struct Record {
int id;
std::string name;
};
bool operator==(const Record& a, const Record& b) {
return a.id == b.id;
}
bool operator!=(const Record& a, const Record& b) {
return !(a == b);
}
bool operator<(const Record& a, const Record& b) {
return a.id < b.id;
}
}
int main() {
Data::Record r1{1, "Alice"};
Data::Record r2{2, "Bob"};
// ADL: Data::operator== 자동 찾음
if (r1 == r2) {
std::cout << "같음" << std::endl;
}
if (r1 < r2) {
std::cout << "r1이 작음" << std::endl;
}
// std::sort도 ADL 사용
std::vector<Data::Record> records = {r2, r1};
std::sort(records.begin(), records.end());
}
std::sort는 std 네임스페이스 안의 템플릿이고, 내부에서 *a < *b 형태로 비교합니다. 이 호출 지점에서 일반 검색으로는 Data::operator<가 보이지 않지만, 인자 타입이 Data::Record이므로 ADL이 찾아 줍니다. operator<를 전역에 두면 앞에서 설명한 이유로 std::sort 안에서 찾지 못할 수 있습니다.
operator==는 id만 비교하는데, name까지 같아야 같은 레코드로 볼지는 설계 선택입니다. std::find, std::unordered_set 같은 표준 알고리즘과 컨테이너가 모두 이 연산자를 쓰므로 의미를 문서로 남겨 두는 것이 좋습니다. C++20부터는 operator!=를 따로 쓰지 않아도 a != b가 !(a == b)로 재작성되고, auto operator<=>(const Record&) const = default;를 쓰면 비교 연산자를 한 번에 만들 수 있습니다.
예시 4: 직렬화
namespace Serialization {
struct Serializer {
std::ostringstream stream;
std::string str() const {
return stream.str();
}
};
struct Point {
int x, y;
};
Serializer& operator<<(Serializer& s, const Point& p) {
s.stream << "{x:" << p.x << ",y:" << p.y << "}";
return s;
}
Serializer& operator<<(Serializer& s, const std::vector<Point>& points) {
s.stream << "[";
for (size_t i = 0; i < points.size(); i++) {
if (i > 0) s.stream << ",";
s << points[i]; // ADL: Serialization::operator<<(Serializer&, const Point&) 호출
}
s.stream << "]";
return s;
}
}
int main() {
Serialization::Serializer s;
Serialization::Point p1{10, 20};
Serialization::Point p2{30, 40};
// ADL: Serialization::operator<< 호출
s << p1;
std::cout << s.str() << std::endl;
std::vector<Serialization::Point> points = {p1, p2};
Serialization::Serializer s2;
s2 << points;
std::cout << s2.str() << std::endl;
}
벡터용 연산자 안의 s << points[i]는 자기 자신을 재귀 호출하는 것이 아니라, 인자 타입이 Point이므로 바로 위의 Point용 오버로드가 선택됩니다. 이 설계의 장점은 새 타입을 직렬화 가능하게 만들 때 Serializer를 고칠 필요 없이, 그 타입과 같은 네임스페이스에 operator<<(Serializer&, const T&)만 추가하면 된다는 것입니다. nlohmann/json의 to_json/from_json이나 Boost.Serialization의 serialize 같은 라이브러리 확장 지점이 모두 이 방식으로 사용자 코드를 찾습니다.
이 예제에서 std::vector<Point>용 오버로드가 찾아지는 이유도 흥미롭습니다. std::vector<Serialization::Point>는 std의 템플릿이지만, 템플릿 인자 Point의 네임스페이스인 Serialization도 연관 네임스페이스에 포함되므로 ADL로 발견됩니다. 첫 인자 Serializer&도 Serialization에 있으니 여기서는 어느 쪽으로든 찾아집니다.
ADL 검색 범위
ADL이 뒤지는 범위는 생각보다 좁습니다. 아래 예제는 컴파일 오류가 나는 경우입니다.
namespace A {
struct X {};
}
namespace B {
void func(A::X) {
std::cout << "B::func" << std::endl;
}
}
namespace C {
void test() {
A::X x;
// func(x); // 에러: C에서 func 못 찾음
// ADL은 A::X의 네임스페이스(A)만 검색
}
}
중첩 네임스페이스
namespace Outer {
namespace Inner {
struct X {};
void func(X) {
std::cout << "Inner::func" << std::endl;
}
}
}
int main() {
Outer::Inner::X x;
// ADL: Outer::Inner::func 찾음
func(x);
}
중첩 네임스페이스에서 ADL은 타입을 직접 감싸는 네임스페이스(Outer::Inner)만 검색하고, 바깥 Outer까지 올라가지 않습니다. 그래서 Outer에 void func(Inner::X)를 두면 ADL로는 찾을 수 없습니다. 라이브러리를 lib::detail, lib::v2처럼 나눌 때 연산자와 헬퍼 함수를 타입과 같은 하위 네임스페이스에 두어야 하는 이유입니다. 예외적으로 inline namespace는 바깥 네임스페이스의 일부로 취급되어 연관 네임스페이스가 함께 잡히는데, 버전 관리용으로 쓰는 방법은 inline namespace에서 다룹니다.
자주 발생하는 문제
문제 1: 의도하지 않은 함수 호출
namespace MyLib {
struct Data {};
void process(Data) {
std::cout << "MyLib::process" << std::endl;
}
}
void process(MyLib::Data) {
std::cout << "global::process" << std::endl;
}
int main() {
MyLib::Data d;
// 모호한 호출: 일반 검색이 ::process, ADL이 MyLib::process를 찾음
// process(d); // error: call of overloaded 'process(MyLib::Data&)' is ambiguous
// 명시적 호출
::process(d); // global::process
}
process(d)는 컴파일되지 않습니다. 일반 검색이 전역 ::process를 찾고, ADL이 MyLib::process를 찾는데, 두 함수의 매개변수가 똑같아 오버로드 해석으로 우열을 가릴 수 없기 때문입니다. ADL은 일반 검색 결과를 대체하는 것이 아니라 더하는 것이라, 양쪽에 같은 시그니처가 있으면 모호성 오류가 납니다.
더 위험한 경우는 컴파일이 되는 쪽입니다. 전역 함수가 process(const MyLib::Data&)이고 MyLib에 process(MyLib::Data) 대신 템플릿 template<class T> void process(T)가 있다고 하면, 인자 변환 순위에 따라 어느 한쪽이 조용히 선택됩니다. 라이브러리를 업데이트했더니 그 라이브러리의 네임스페이스에 같은 이름의 함수가 새로 생겨 내 코드의 호출 대상이 바뀌는 일도 이 경로로 일어납니다. std::의 타입을 인자로 넘기는 한정되지 않은 호출은 표준 라이브러리 구현이 추가한 내부 함수와 이름이 겹칠 수 있다는 점도 기억해 둘 만합니다.
문제 2: 템플릿과 ADL
namespace MyLib {
struct X {};
void func(X) {
std::cout << "MyLib::func" << std::endl;
}
}
template<typename T>
void call(T value) {
func(value); // ADL 작동
}
int main() {
MyLib::X x;
call(x); // MyLib::func 호출
}
템플릿 안의 func(value)는 T에 의존하는 이름이라, 템플릿을 정의하는 시점이 아니라 T가 정해지는 인스턴스화 시점에 ADL 검색이 한 번 더 일어납니다(two-phase lookup). 이 예제에서 call 정의 시점에는 func가 하나도 보이지 않지만, T = MyLib::X로 인스턴스화될 때 ADL이 MyLib::func를 찾아 컴파일됩니다. call(42)처럼 기본 타입을 넘기면 ADL이 뒤질 네임스페이스가 없어 인스턴스화 지점에서 오류가 납니다. GCC는 이때 “‘func’ was not declared in this scope, and no declarations were found by argument-dependent lookup at the point of instantiation”이라고 원인을 꽤 친절하게 알려 줍니다.
이 성질은 양날의 검입니다. 템플릿 작성자가 사용자 타입마다 동작을 바꿀 수 있는 확장 지점을 만들 수 있지만, 반대로 템플릿 작성자가 의도한 헬퍼 함수 대신 사용자 네임스페이스의 동명 함수가 끼어들 수도 있습니다. 템플릿 안에서 특정 함수를 반드시 호출해야 한다면 detail::func(value)처럼 한정해서 ADL을 끄는 것이 안전합니다.
C++17까지는 func<int>(x)처럼 명시적 템플릿 인자를 주면, 그 위치에서 func라는 템플릿 이름이 일반 검색으로 보이지 않는 한 <를 비교 연산자로 해석해 ADL이 동작하지 않았습니다. C++20(P0846)부터는 이 경우에도 ADL이 적용됩니다.
문제 3: using 선언과 충돌
namespace A {
struct X {};
void func(X) { std::cout << "A::func" << std::endl; }
}
namespace B {
void func(A::X) { std::cout << "B::func" << std::endl; }
}
int main() {
using B::func;
A::X x;
func(x); // 모호함! A::func vs B::func
}
블록 안의 using B::func;는 일반 검색 결과를 B::func로 만들 뿐, ADL을 끄지 않습니다. 그래서 ADL이 찾은 A::func와 경쟁해 모호성 오류가 납니다. 반면 블록 스코프에 void func(A::X);처럼 함수 선언 자체를 두면 일반 검색이 그 선언을 찾은 시점에 ADL이 생략됩니다. 또 일반 검색 결과가 함수가 아닌 이름(변수나 함수 객체)이면 ADL은 수행되지 않습니다. auto func = [](A::X) {};를 먼저 선언하면 람다가 호출되는 이유가 이것이며, C++20의 std::ranges 알고리즘이 함수 대신 함수 객체로 구현된 것도 ADL을 의도적으로 피하기 위해서입니다.
문제 4: 빌트인 타입
void func(int) {
std::cout << "func(int)" << std::endl;
}
namespace MyLib {
void func(int) {
std::cout << "MyLib::func(int)" << std::endl;
}
}
int main() {
int x = 10;
// ADL 작동 안함 (int는 네임스페이스 없음)
func(x); // global::func 호출
// 명시적 호출 필요
MyLib::func(x);
}
int, double, 포인터 같은 기본 타입은 어떤 네임스페이스에도 속하지 않으므로 연관 네임스페이스가 없고, 일반 검색만 일어납니다. typedef나 using 별칭도 새 타입을 만들지 않기 때문에, namespace MyLib { using Id = int; }로 선언한 MyLib::Id를 넘겨도 MyLib은 검색되지 않습니다. 반대로 enum class는 선언된 네임스페이스에 속한 진짜 타입이라 ADL 대상이 됩니다. ID처럼 기본 타입을 감싼 값에 전용 연산을 붙이고 싶다면 별칭 대신 작은 구조체나 enum class로 강한 타입을 만드는 편이 ADL과도 잘 맞습니다.
ADL 비활성화
namespace MyLib {
struct X {};
void func(X) { std::cout << "MyLib::func" << std::endl; }
}
void func(MyLib::X) {
std::cout << "global::func" << std::endl;
}
int main() {
MyLib::X x;
// func(x); // 모호함: 일반 검색(::func) + ADL(MyLib::func)
(func)(x); // ADL 비활성화: global::func
::func(x); // 명시적: global::func
}
함수 이름을 괄호로 감싸면 더 이상 “한정되지 않은 함수 이름으로 된 호출”이 아니게 되어 ADL이 적용되지 않습니다. 그래서 (func)(x)는 일반 검색으로 찾은 전역 func만 후보로 남아 컴파일됩니다. 반면 괄호 없는 func(x)는 문제 1과 같은 이유로 모호성 오류가 납니다.
괄호 방식은 표준에 정의된 동작이지만 코드 리뷰에서 의도를 알아보기 어렵습니다. 실무에서는 ::func(x)나 Lib::func(x)처럼 네임스페이스로 한정하는 방법이 더 흔하고, 매크로와 이름이 겹치는 (std::max)(a, b) 같은 경우에 괄호가 쓰이는 정도입니다. Windows 헤더의 max 매크로를 피하려고 쓰는 이 패턴도 부수적으로 ADL과 매크로 확장을 함께 막습니다.
타입과 같은 네임스페이스에 둘 함수, 한정해서 부를 함수
namespace MyLib {
struct Point {
int x, y;
};
// ✅ 같은 네임스페이스에 연산자 정의
Point operator+(const Point& a, const Point& b) {
return {a.x + b.x, a.y + b.y};
}
// ✅ swap도 같은 네임스페이스
void swap(Point& a, Point& b) noexcept {
std::swap(a.x, b.x);
std::swap(a.y, b.y);
}
// ✅ 스트림 연산자
std::ostream& operator<<(std::ostream& os, const Point& p) {
return os << "(" << p.x << ", " << p.y << ")";
}
}
// ❌ 전역 네임스페이스에 정의하지 말 것
// Point operator+(const MyLib::Point& a, const MyLib::Point& b) { ... }
정리하면 ADL을 다루는 기준은 두 가지입니다. 타입의 인터페이스에 속하는 함수(연산자, swap, to_string, begin/end, 직렬화 훅)는 타입과 같은 네임스페이스에 두어 ADL로 찾히게 하고, 반대로 내부 구현용 헬퍼를 호출할 때는 네임스페이스로 한정해서 ADL이 끼어들 여지를 없앱니다. 헤더에서 using namespace를 쓰면 일반 검색 범위까지 넓어져 앞에서 본 모호성 문제가 사용자 코드 전체로 퍼지므로 피해야 합니다. 네임스페이스 설계 자체는 namespace 가이드를 참고하세요.
FAQ
Q1: ADL은 언제 작동하나요?
A: 네임스페이스로 한정하지 않은 함수 호출(f(x))과 연산자 표현식(a + b, os << x)에서 작동합니다. 일반 검색 결과가 블록 스코프 함수 선언이거나 함수가 아닌 이름(변수, 함수 객체)이면 생략됩니다.
Q2: 이런 규칙이 왜 필요한가요?
A: 연산자에는 네임스페이스를 붙일 문법이 없어서, 타입과 함께 정의한 연산자를 어디서든 쓰려면 인자 타입을 기준으로 찾는 규칙이 필요합니다. 템플릿 라이브러리가 사용자 타입의 swap, begin, 직렬화 함수 같은 확장 지점을 찾는 것도 같은 규칙입니다.
Q3: ADL을 끄려면 어떻게 하나요?
A: ::func(x)나 Lib::func(x)처럼 한정해서 호출하는 방법이 가장 명확합니다. (func)(x)처럼 이름을 괄호로 감싸도 ADL이 꺼집니다.
Q4: int 같은 기본 타입에도 ADL이 적용되나요?
A: int, double, 포인터 같은 기본 타입과 그 별칭(using Id = int)은 연관 네임스페이스가 없어 ADL이 추가로 검색하는 곳이 없습니다.
Q5: ADL 때문에 생기는 버그를 어떻게 예방하나요?
A: 일반 검색과 ADL이 같은 시그니처를 각각 찾으면 모호성 오류가 나고, 조금 더 잘 맞는 오버로드가 다른 네임스페이스에 있으면 조용히 그쪽이 선택됩니다. 내부 헬퍼는 한정해서 호출하고, 헤더에서 using namespace를 쓰지 않는 것이 기본 방어입니다.
같이 보면 좋은 글
- C++ namespace 심화
- C++ 연산자 오버로딩: 멤버·비멤버 선택, 산술·비교·입출력 연산자 구현
- C++ 연산자 우선순위 함정: flags & MASK == 0 같은 비트·비교·논리 연산자 버그
- C++ 함수 객체
- C++ User-Defined Literals