오버로드 해석

오버로드 해석 (Overload resolution)

같은 이름의 함수가 여럿 있으면(오버로드 집합), 호출할 때 어느 걸 쓸지는 컴파일러가 정해야 해요. 그 과정이 **오버로드 해석(overload resolution)**이에요.

함수 호출을 컴파일하려면 컴파일러는 먼저 이름 룩업(name lookup)을 해요. 함수의 경우 여기에 인자 의존 룩업(ADL)이 포함될 수 있고, 함수 템플릿은 그 뒤에 템플릿 인자 추론(template argument deduction)이 이어질 수 있어요.

이름이 둘 이상의 실체를 가리키면 그 이름은 **오버로드(overloaded)**됐다고 해요. 그러면 컴파일러는 어떤 오버로드를 호출할지 결정해야 해요. 간단히 말하면, 매개변수가 인자와 가장 가깝게 일치하는 오버로드가 호출돼요.

자세히 보면 오버로드 해석은 다음 단계로 진행돼요.

  1. 후보 함수(candidate functions) 집합을 만든다.
  2. 그 집합을 유효 함수(viable functions)만으로 줄인다.
  3. 그 집합을 분석해 단 하나의 최고 유효 함수(best viable function)를 정한다(여기에 암시적 변환 시퀀스 순위 매기기가 포함될 수 있어요).
void f(long);
void f(float);

f(0L); // calls f(long)
f(0);  // error: ambiguous overload

함수 호출 외에도, 오버로드된 함수 이름은 몇 가지 추가 문맥에서 나타날 수 있는데, 거기엔 다른 규칙이 적용돼요. 오버로드된 함수의 주소(Address of an overloaded function)를 참고해요.

오버로드 해석으로 함수를 고를 수 없으면 그 함수는 쓸 수 없어요(예: 실패한 제약(constraint)을 가진 템플릿 실체).

출처: cppreference

본문

후보 함수(Candidate functions)

오버로드 해석이 시작되기 전에, 이름 룩업과 템플릿 인자 추론이 고른 함수들을 합쳐 후보 함수(candidate function) 집합을 만들어요. 정확한 세부 사항은 오버로드 해석이 일어나는 문맥에 따라 달라져요.

이름 붙은 함수 호출

함수 호출 표현식 E(args)에서 E가 오버로드된 함수·함수 템플릿 집합(호출 가능한 객체는 아님)을 가리키면 다음 규칙이 적용돼요.

  • EPA->BA.B 형태(A는 클래스 타입 cv T)면:
    • Bsplice 표현식이면 B가 가리키는 함수 선언들이 후보 함수예요. (C++26부터)
    • 그 외에는 BT의 멤버 함수로 룩업해요. 그 룩업이 찾은 함수 선언들이 후보 함수예요.
    • 오버로드 해석을 위한 인자 목록에는 타입 cv T의 암시적 객체 인자(implied object argument)가 포함돼요.
  • E기본 표현식(primary expression)이면:
    • Esplice 표현식이면 E가 가리키는 함수 선언들이 후보 함수예요. (C++26부터)
    • 그 외에는 이름을 함수 호출의 일반 규칙에 따라 룩업해요(ADL 포함 가능). 이 룩업이 찾은 함수 선언들은 (룩업 방식 때문에) 다음 중 하나예요.
      • 전부 비멤버 함수(이 경우 오버로드 해석을 위한 인자 목록은 함수 호출 표현식에 쓰인 인자 목록과 정확히 같아요).
      • 어떤 클래스 T의 멤버 함수 전부. 이때 this가 스코프 안에 있고 TT의 파생 클래스를 가리키는 포인터라면 *this가 암시적 객체 인자로 쓰여요. 그 외에는(스코프 안에 없거나 T를 가리키지 않으면) 가짜 객체가 암시적 객체 인자로 쓰이고, 오버로드 해석이 결과적으로 비정적 멤버 함수를 고르면 프로그램은 잘못된 형태(ill-formed)예요.

클래스 객체 호출

함수 호출 표현식 E(args)에서 E가 클래스 타입 cv T면:

  • T의 함수 호출 연산자들은 (E).operator() 문맥에서 operator() 이름을 보통 룩업해 얻고, 찾은 모든 선언이 후보 함수 집합에 추가돼요.
  • T 또는 T의 기반 클래스(숨겨지지 않은 경우)에 있는 각 비-explicit 사용자 정의 변환 함수 중, 그 cv 제한자가 T의 cv 제한자와 같거나 더 크고, 다음으로 변환되는 것이면:
    • 함수 포인터
    • 함수 포인터에 대한 참조
    • 함수에 대한 참조
  • 그 변환의 결과가 받아들이는 매개변수 목록을 나머지 매개변수로, 변환 결과의 반환 타입을 반환 타입으로 하는, 고유한 이름의 **대리 호출 함수(surrogate call function)**가 후보 함수 집합에 추가돼요. 이 대리 함수가 이후 오버로드 해석에서 선택되면, 사용자 정의 변환 함수가 호출되고 나서 그 변환 결과가 호출돼요.

어느 경우든 오버로드 해석을 위한 인자 목록은 함수 호출 표현식의 인자 목록 앞에 암시적 객체 인자 E가 붙은 거예요(대리 함수와 맞출 때, 사용자 정의 변환이 암시적 객체 인자를 대리 함수의 첫 번째 인자로 자동 변환해요).

int f1(int);
int f2(float);

struct A
{
    using fp1 = int(*)(int);
    operator fp1() { return f1; } // conversion function to pointer to function
    using fp2 = int(*)(float);
    operator fp2() { return f2; } // conversion function to pointer to function
} a;

int i = a(1); // calls f1 via pointer returned from conversion function

오버로드된 연산자 호출

표현식에서 연산자의 인자 중 적어도 하나가 클래스 타입이나 열거 타입이면, 내장 연산자사용자 정의 연산자 오버로드 모두 오버로드 해석에 참여해요. 후보 함수 집합은 다음과 같이 정해요.

단항 연산자 @(cv 제거 후 타입 T1)나 이항 연산자 @(왼쪽 피연산자 타입 T1, 오른쪽 T2, 둘 다 cv 제거 후)에 대해 다음 후보 집합들이 준비돼요.

  1. 멤버 후보(member candidates): T1이 완전한 클래스이거나 현재 정의 중인 클래스면, 멤버 후보 집합은 T1::operator@를 한정 이름 룩업한 결과예요. 그 외에는 멤버 후보 집합은 비어 있어요.

  2. 비멤버 후보(non-member candidates): 연산자 오버로딩이 비멤버 형태를 허용하는 연산자에 대해, 표현식 문맥에서 operator@를 비한정 이름 룩업(ADL 포함 가능)해 찾은 모든 선언이 후보예요. 다만 멤버 함수 선언은 무시되고, 그 선언들이 다음 바깥 스코프로 룩업을 계속하는 걸 막지는 않아요. 이항 연산자의 두 피연산자나 단항 연산자의 유일한 피연산자가 열거 타입이면, 룩업 집합에서 열거 타입(또는 그 열거 타입에 대한 참조)을 매개변수로 갖는 함수만 비멤버 후보가 돼요.

  3. 내장 후보(built-in candidates): operator,, 단항 operator&, operator->는 내장 후보 집합이 비어 있어요. 다른 연산자는 모든 피연산자가 그 매개변수로 암시적으로 변환될 수 있는 한 내장 연산자 페이지에 나열된 것들이 내장 후보예요. 어떤 내장 후보가 함수 템플릿 특수화가 아닌 비멤버 후보나 재작성 비멤버 후보와 같은 매개변수 목록을 가지면, 그 내장 후보는 내장 후보 목록에 추가되지 않아요. (C++20부터) 내장 대입 연산자를 고려할 땐 첫 번째 매개변수로부터의 변환이 제한돼요. 표준 변환 시퀀스만 고려돼요.

4) 재작성 후보(rewritten candidates):
- 네 관계 연산자 표현식 x < y, x <= y, x > y, x >= y에 대해, 찾은 모든 멤버·비멤버·내장 operator<=>가 집합에 추가돼요.
- 네 관계 연산자 표현식과 삼중 비교 표현식 x <=> y에 대해, 찾은 각 멤버·비멤버·내장 operator<=>마다 두 매개변수의 순서를 뒤집은 **합성 후보(synthesized candidate)**가 추가돼요.
- x != y에 대해, 일치하는 operator!=가 없으면 찾은 모든 멤버·비멤버·내장 operator==가 집합에 추가돼요.
- 동등 연산자 표현식 x == yx != y에 대해, 일치하는 operator!=가 없으면 찾은 각 멤버·비멤버·내장 operator==마다 매개변수 순서를 뒤집은 합성 후보가 추가돼요.
모든 경우에 재작성 후보는 재작성된 표현식의 문맥에서는 고려되지 않아요. 다른 모든 연산자는 재작성 후보 집합이 비어 있어요.
(C++20부터)

오버로드 해석에 제출될 후보 함수 집합은 위 집합들의 합집합이에요. 오버로드 해석을 위한 인자 목록은 연산자의 피연산자들이에요. 다만 operator->는 두 번째 피연산자가 함수 호출의 인자가 아니에요(멤버 접근 연산자 참고).

struct A
{
    operator int();              // user-defined conversion
};
A operator+(const A&, const A&); // non-member user-defined operator

void m()
{
    A a, b;
    a + b; // member-candidates: none
           // non-member candidates: operator+(a, b)
           // built-in candidates: int(a) + int(b)
           // overload resolution chooses operator+(a, b)
}

오버로드 해석이 내장 후보를 고르면, 클래스 타입 피연산자로부터의 사용자 정의 변환 시퀀스가 두 번째 표준 변환 시퀀스를 가질 수 없어요. 사용자 정의 변환 함수가 기대되는 피연산자 타입을 바로 주어야 해요.

struct Y { operator int*(); }; // Y is convertible to int*
int *a = Y() + 100.0;          // error: no operator+ between pointer and double

operator,, 단항 operator&, operator->의 경우, 후보 함수 집합에 유효 함수(아래 참고)가 없으면 그 연산자는 내장으로 재해석돼요.

연산자 @에 대해 오버로드 해석이 재작성 operator<=> 후보를 고르면, x @ y는 재작성 표현식으로 해석돼요. 선택된 후보가 매개변수 순서를 뒤집은 합성 후보면 0 @ (y <=> x), 아니면 (x <=> y) @ 0으로요.
연산자 @(== 또는 !=)에 대해 오버로드 해석이 재작성 operator== 후보를 고르면, 그 반환 타입은 (cv 제한된) bool이어야 하고, x @ y는 선택된 합성 후보가 매개변수 순서를 뒤집었으면 y == x 또는 !(y == x), 아니면 !(x == y)로 해석돼요.
이 경우 오버로드 해석은 재작성 후보보다 비재작성 후보를, 합성 재작성 후보보다 비합성 재작성 후보를 선호하는 최종 타이브레이커를 가져요.
이 역순 인자 룩업 덕분에 operator<=>(std::string, const char*)operator==(std::string, const char*)만 작성해도 std::stringconst char* 사이의 모든 비교를 양방향으로 생성할 수 있어요. 자세한 내용은 기본 비교(default comparisons)를 참고해요.
(C++20부터)

생성자에 의한 초기화

클래스 타입 객체가 직접 초기화(direct-initialized)되거나 기본 초기화될 때(복사 목록 초기화 문맥의 기본 초기화 포함, C++11부터), 후보 함수는 초기화되는 클래스의 모든 생성자예요. 인자 목록은 초기화식의 표현식 목록이에요.

그 외에는 후보 함수는 초기화되는 클래스의 모든 변환 생성자(converting constructor)예요. 인자 목록은 초기화식의 표현식이에요.

복사 목록 초기화 문맥의 기본 초기화에서 explicit 생성자가 선택되면 그 초기화는 잘못된 형태예요. (C++11부터)

변환에 의한 복사 초기화

클래스 타입 객체의 복사 초기화가 타입 cv S의 초기화식을 초기화되는 객체의 타입 cv T로 변환하기 위해 사용자 정의 변환을 호출해야 한다면, 다음 함수들이 후보 함수예요.

  • T의 모든 변환 생성자
  • S와 그 기반 클래스(숨겨지지 않은 경우)의 비-explicit 변환 함수 중 TT에서 파생된 클래스, 또는 그에 대한 참조로 변환하는 것. 이 복사 초기화가 cv T의 직접 초기화 시퀀스의 일부(cv T에 대한 참조를 받는 생성자의 첫 번째 매개변수에 바인딩할 참조를 초기화하는 경우)면, explicit 변환 함수도 고려돼요.

어느 경우든 오버로드 해석을 위한 인자 목록은 초기화식이라는 단일 인자로 구성돼요. 이 인자는 생성자의 첫 번째 인자나 변환 함수의 암시적 객체 인자와 비교돼요.

변환에 의한 비-클래스 초기화

비-클래스 타입 cv1 T 객체의 초기화가 클래스 타입 cv S의 초기화식에서 변환하기 위해 사용자 정의 변환 함수를 필요로 하면, 다음 함수들이 후보예요.

  • S와 그 기반 클래스(숨겨지지 않은 경우)의 비-explicit 사용자 정의 변환 함수 중, 타입 T표준 변환 시퀀스T로 변환 가능한 타입, 또는 그런 타입에 대한 참조를 만드는 것. 후보 함수 선택에선 반환 타입의 cv 제한자는 무시돼요.
  • 이것이 직접 초기화면, S와 그 기반 클래스의 explicit 사용자 정의 변환 함수 중 타입 T자격 변환(qualification conversion)으로 T로 변환 가능한 타입, 또는 그런 타입에 대한 참조를 만드는 것도 고려돼요.

어느 경우든 오버로드 해석을 위한 인자 목록은 초기화식이라는 단일 인자로 구성되고, 변환 함수의 암시적 객체 인자와 비교돼요.

변환에 의한 참조 초기화

참조 초기화 동안 cv1 T에 대한 참조가 클래스 타입 cv2 S의 초기화식에서 변환한 좌값이나 우값 결과에 바인딩될 때, 다음 함수들이 후보 집합으로 선택돼요.

  • S와 그 기반 클래스(숨겨지지 않은 경우)의 비-explicit 사용자 정의 변환 함수 중:
    • (좌값으로 변환할 때) cv2 T2에 대한 좌값 참조
    • (우값이나 함수 타입의 좌값으로 변환할 때) cv2 T2 또는 cv2 T2에 대한 우값 참조
  • 여기서 cv2 T2cv1 T참조 호환(reference-compatible)이에요.
  • 직접 초기화에선, T2T와 같은 타입이거나 자격 변환으로 T로 변환될 수 있으면 explicit 사용자 정의 변환 함수도 고려돼요.

어느 경우든 오버로드 해석을 위한 인자 목록은 초기화식이라는 단일 인자로 구성되고, 변환 함수의 암시적 객체 인자와 비교돼요.

목록 초기화(List-initialization)

비-집계 클래스 타입 T 객체가 목록 초기화되면 2단계 오버로드 해석이 일어나요.

  • 1단계에서 후보 함수는 T의 모든 initializer-list 생성자이고, 오버로드 해석을 위한 인자 목록은 단일 initializer-list 인자예요.
  • 1단계 오버로드 해석이 실패하면 2단계로 들어가는데, 후보 함수는 T의 모든 생성자이고 인자 목록은 initializer-list의 개별 요소들이에요.

initializer-list가 비어 있고 T에 기본 생성자가 있으면 1단계는 건너뛰어요.

복사 목록 초기화에서 2단계가 explicit 생성자를 선택하면 그 초기화는 잘못된 형태예요(다른 모든 복사 초기화에서 explicit 생성자는 아예 고려되지 않는 것과 대조돼요).

함수 템플릿 후보에 대한 추가 규칙

이름 룩업이 함수 템플릿을 찾았으면, 이 경우에 쓸 수 있는 템플릿 인자 값(있으면)을 찾기 위해 템플릿 인자 추론과 명시적 템플릿 인자 확인을 수행해요.

  • 둘 다 성공하면, 템플릿 인자로 대응하는 함수 템플릿 특수화의 선언을 합성해 후보 집합에 추가하고, 아래 타이브레이커 규칙에서 별도로 명시하지 않는 한 그 특수화를 비템플릿 함수처럼 취급해요.
  • 인자 추론이 실패하거나 합성된 함수 템플릿 특수화가 잘못된 형태면, 그런 함수는 후보 집합에 추가되지 않아요(SFINAE 참고).

이름이 하나 이상의 함수 템플릿과 오버로드된 비템플릿 함수 집합을 동시에 가리키면, 그 함수들과 템플릿에서 생성된 특수화 모두가 후보예요.

자세한 내용은 함수 템플릿 오버로딩을 참고해요.

생성자 템플릿이나 변환 함수 템플릿이 값 의존적(value-dependent)조건부 explicit 지정자를 가지면, 추론 후 문맥이 비-explicit 후보를 필요로 하는데 생성된 특수화가 explicit이면, 그 특수화는 후보 집합에서 제거돼요. (C++20부터)

생성자 후보에 대한 추가 규칙

deleted로 정의된 기본 이동 생성자이동 대입 연산자는 후보 함수 집합에서 제외돼요.
클래스 타입 C에서 상속된 생성자 중 첫 번째 매개변수 타입이 "P에 대한 참조"인 생성자(템플릿에서 인스턴스화된 것 포함)는, 다음 조건을 모두 만족할 때 타입 D의 객체를 만들 때 후보 집합에서 제외돼요.
- 인자 목록에 인자가 정확히 하나다.
- CP참조 관련(reference-related)이다.
- PD와 참조 관련이다.
(C++11부터)

멤버 함수 후보에 대한 추가 규칙

어떤 후보 함수가 명시적 객체 매개변수(C++23부터)가 없는 (정적·비정적) 멤버 함수이고 생성자가 아니면, 실제 매개변수 첫 번째 앞에 호출되는 객체를 나타내는 추가 매개변수(암시적 객체 매개변수, implicit object parameter)를 가진 것처럼 취급돼요.

마찬가지로 멤버 함수가 호출되는 객체는 **암시적 객체 인자(implied object argument)**로 인자 목록 맨 앞에 붙어요.

클래스 X의 멤버 함수에 대해, 암시적 객체 매개변수의 타입은 멤버 함수에서 설명한 대로 멤버 함수의 cv 제한자와 참조 제한자(ref-qualifier)의 영향을 받아요.

암시적 객체 매개변수의 타입을 정할 때 사용자 정의 변환 함수는 _암시적 객체 인자_의 멤버로 간주돼요.

using-선언이 파생 클래스에 도입한 멤버 함수는 암시적 객체 매개변수의 타입을 정의할 때 파생 클래스의 멤버로 간주돼요.

정적 멤버 함수의 경우 암시적 객체 매개변수는 어떤 객체와도 일치한다고 간주돼요. 그 타입은 검사되지 않고 그에 대한 변환 시퀀스도 시도되지 않아요. (C++23까지)

오버로드 해석의 나머지에서 암시적 객체 인자는 다른 인자와 구별되지 않지만, 암시적 객체 매개변수에는 다음 특별 규칙이 적용돼요.

  1. 암시적 객체 매개변수에는 사용자 정의 변환을 적용할 수 없어요.
  2. 우값(우측 값)은 비-const 암시적 객체 매개변수에 바인딩될 수 있고(참조 제한된 멤버 함수가 아닌 경우, C++11부터), 암시적 변환의 순위에는 영향을 주지 않아요.
struct B { void f(int); };
struct A { operator B&(); };

A a;
a.B::f(1); // Error: user-defined conversions cannot be applied
           // to the implicit object parameter
static_cast<B&>(a).f(1); // OK

유효 함수(Viable functions)

위에서 설명한 대로 만들어진 후보 함수 집합이 있으면, 오버로드 해석의 다음 단계는 인자와 매개변수를 검사해 그 집합을 유효 함수(viable function) 집합으로 줄이는 것이에요.

유효 함수 집합에 포함되려면 후보 함수는 다음을 만족해야 해요.

  1. 인자가 M개면, 정확히 M개의 매개변수를 가진 후보 함수가 유효해요.
  2. 후보 함수가 M개보다 적은 매개변수를 가지지만 가변 인자(ellipsis) 매개변수가 있으면 유효해요.
  3. 후보 함수가 M개보다 많은 매개변수를 갖고, M+1 번째 이후 모든 매개변수에 기본 인자가 있으면 유효해요. 오버로드 해석의 나머지에서 매개변수 목록은 M개에서 잘려요.
  4. 함수에 제약(constraint)이 있으면 그 제약이 충족돼야 해요. (C++20부터)
  5. 모든 인자에 대해 그것을 대응하는 매개변수로 변환할 암시적 변환 시퀀스가 적어도 하나 있어야 해요.
  6. 어떤 매개변수가 참조 타입이면 참조 바인딩이 이 단계에서 고려돼요. 우값 인자가 비-const 좌값 참조 매개변수에 대응하거나, 좌값 인자가 우값 참조 매개변수에 대응하면 그 함수는 유효하지 않아요.

사용자 정의 변환(변환 생성자와 사용자 정의 변환 함수 모두)은 하나 이상의 사용자 정의 변환을 적용할 수 있게 만드는 암시적 변환 시퀀스에 참여하는 게 금지돼요. 구체적으로, 변환 대상이 생성자의 첫 번째 매개변수나 사용자 정의 변환 함수의 암시적 객체 매개변수이고, 그 생성자/사용자 정의 변환이 다음의 후보라면 고려되지 않아요.

- initializer-list가 정확히 한 요소를 가진 initializer-list 자신이고, 대상이 클래스 X의 생성자의 첫 번째 매개변수이며, 변환이 X나 (cv 제한된) X에 대한 참조로의 변환인 목록 초기화에 의한 초기화:
<br>struct A { A(int); };<br>struct B { B(A); };<br>B b{{0}}; // list-initialization of B<br>// candidates: B(const B&), B(B&&), B(A)<br>// {0} -> B&& not viable: would have to call B(A)<br>// {0} -> const B&: not viable: would have to bind to rvalue, would have to call B(A)<br>// {0} -> A viable. Calls A(int): user-defined conversion to A is not banned<br>
(C++11부터)

최고 유효 함수(Best viable function)

유효 함수 F1F2의 각 쌍에 대해, i 번째 인자에서 i 번째 매개변수로의 암시적 변환 시퀀스의 순위를 매겨 어느 쪽이 더 나은지 정해요(첫 번째 인자와 정적 멤버 함수의 암시적 객체 인자는 순위에 영향 없음).

F1F2보다 나은 함수로 판정되려면, F1의 모든 인자에 대한 암시적 변환이 F2의 모든 인자에 대한 암시적 변환보다 나쁘지 않고, 다음 중 하나를 만족해야 해요.

  1. F1의 어떤 인자의 암시적 변환이 F2의 해당 인자 암시적 변환보다 더 낫다. 그게 아니면,
  2. (변환에 의한 비-클래스 초기화 문맥에서만) 초기화되는 타입으로의 F1 결과 표준 변환 시퀀스가 F2 결과의 것보다 더 낫다. 그게 아니면,
  3. (함수 타입 참조의 직접 참조 바인딩을 위한 변환 함수 초기화 문맥에서만, C++11부터) F1의 결과가 초기화되는 참조와 같은 종류의 참조(좌값·우값)인데 F2의 결과는 아니다. 그게 아니면,
  4. F1이 비템플릿 함수이고 F2는 템플릿 특수화다. 그게 아니면,
  5. F1F2가 모두 템플릿 특수화이고, 템플릿 특수화 부분 순서 규칙에 따라 F1이 더 특수화됐다. 그게 아니면,
  6. F1F2가 비템플릿 함수이고, F1F2보다 부분 순서에서 더 제약됨. (C++20부터) 그게 아니면,
  7. F1이 클래스 D의 생성자이고 F2D의 기반 클래스 B의 생성자이며, 모든 인자에 대해 F1F2의 대응 매개변수 타입이 같다. (C++11부터) 그게 아니면,
  8. F2가 재작성 후보이고 F1은 아니다. 그게 아니면,
  9. F1F2가 모두 재작성 후보이고, F2가 매개변수 순서를 뒤집은 합성 재작성 후보인데 F1은 아니다. (C++20부터) 그게 아니면,
  10. F1사용자 정의 추론 가이드에서 생성됐고 F2는 아니다. 그게 아니면,
  11. F1복사 추론 후보이고 F2는 아니다. 그게 아니면,
  12. F1이 비템플릿 생성자에서 생성됐고 F2가 생성자 템플릿에서 생성됐다. (C++17부터)

이 쌍별 비교는 모든 유효 함수에 적용돼요. 정확히 하나의 유효 함수가 다른 모든 것보다 나으면 오버로드 해석은 성공하고 그 함수가 호출돼요. 그렇지 않으면 컴파일이 실패해요.

void Fcn(const int*, short); // overload #1
void Fcn(int*, int);         // overload #2

int i;
short s = 0;

void f()
{
    Fcn(&i, 1L);  // 1st argument: &i -> int* is better than &i -> const int*
                  // 2nd argument: 1L -> short and 1L -> int are equivalent
                  // calls Fcn(int*, int)

    Fcn(&i, 'c'); // 1st argument: &i -> int* is better than &i -> const int*
                  // 2nd argument: 'c' -> int is better than 'c' -> short
                  // calls Fcn(int*, int)

    Fcn(&i, s);   // 1st argument: &i -> int* is better than &i -> const int*
                  // 2nd argument: s -> short is better than s -> int
                  // no winner, compilation error
}

최고 유효 함수가 여러 선언이 발견된 함수로 결정됐는데, 그 선언들 중 둘이 서로 다른 스코프에 있고 그 함수를 유효하게 만든 기본 인자를 지정한다면, 프로그램은 잘못된 형태예요.

namespace A
{
    extern "C" void f(int = 5);
}

namespace B
{
    extern "C" void f(int = 5);
}

using A::f;
using B::f;

void use()
{
    f(3); // OK, default argument was not used for viability
    f();  // error: found default argument twice
}

암시적 변환 시퀀스 순위 매기기(Ranking of implicit conversion sequences)

오버로드 해석이 고려하는 인자-매개변수 암시적 변환 시퀀스는 (비참조 매개변수에 대해) 복사 초기화에서 쓰는 암시적 변환과 대응돼요. 다만 암시적 객체 매개변수나 대입 연산자 왼쪽으로의 변환을 고려할 때, 임시 객체를 만드는 변환은 고려되지 않아요. 매개변수가 정적 멤버 함수의 암시적 객체 매개변수면, 그 암시적 변환 시퀀스는 다른 표준 변환 시퀀스보다 더 낫지도 나쁘지도 않은 표준 변환 시퀀스예요. (C++23부터)

표준 변환 시퀀스 유형에는 세 가지 순위 중 하나가 배정돼요.

  1. 정확 일치(Exact match): 변환 불필요, 좌값→우값 변환, 자격 변환, 함수 포인터 변환, (C++17부터) 클래스 타입에서 같은 클래스로의 사용자 정의 변환.
  2. 승격(Promotion): 정수 승격, 부동소수점 승격.
  3. 변환(Conversion): 정수 변환, 부동소수점 변환, 부동소수점-정수 변환, 포인터 변환, 멤버 포인터 변환, 불리언 변환, 파생 클래스에서 기반 클래스로의 사용자 정의 변환.

표준 변환 시퀀스의 순위는 그 안에 든 표준 변환들의 순위 중 가장 나쁜 것이에요(최대 세 개의 변환이 있을 수 있어요).

참조 매개변수를 인자 표현식에 직접 바인딩하는 것은 identity이거나 파생→기반 변환이에요.

struct Base {};
struct Derived : Base {} d;

int f(Base&);    // overload #1
int f(Derived&); // overload #2

int i = f(d); // d -> Derived& has rank Exact Match
              // d -> Base& has rank Conversion
              // calls f(Derived&)

변환 시퀀스 순위가 타입과 값 카테고리만으로 동작하기 때문에, 비트 필드(bit field)는 순위 매기기를 위해 참조 인자에 바인딩될 수 있어요. 하지만 그 함수가 선택되면 잘못된 형태가 돼요.

  1. 표준 변환 시퀀스는 항상 사용자 정의 변환 시퀀스나 ellipsis 변환 시퀀스보다 낫다.

  2. 사용자 정의 변환 시퀀스는 항상 ellipsis 변환 시퀀스보다 낫다.

  3. 표준 변환 시퀀스 S1S2보다 낫다: a) S1이 좌값 변환을 제외한 S2의 진부분 시퀀스다. identity 변환 시퀀스는 모든 비-identity 변환의 부분 시퀀스로 간주된다. 그게 아니면, b) S1의 순위가 S2의 순위보다 낫다. 그게 아니면, c) S1S2가 모두 참조 제한된 멤버 함수의 암시적 객체 매개변수가 아닌 참조 매개변수에 바인딩되고, S1은 우값에 우값 참조를, S2는 우값에 좌값 참조를 바인딩한다. d) S1S2가 모두 참조 매개변수에 바인딩되고, S1은 함수에 좌값 참조를, S2는 함수에 우값 참조를 바인딩한다. e) S1S2가 자격 변환에서만 다르고, S1의 결과 cv 제한이 S2 결과의 cv 제한의 진부분집합이며 S1폐기된 문자열 리터럴 배열→포인터 변환이 아니다. (C++20까지) / S1의 결과가 자격 변환으로 S2의 결과로 변환될 수 있다. (C++20부터) f) S1S2가 모두 최상위 cv 제한만 다른 참조 매개변수에 바인딩되고, S1의 타입이 S2보다 cv 제한이 하다. g) S1S2가 같은 참조 타입 "T에 대한 참조"를 바인딩하고 소스 타입이 각각 V1, V2인데, V1*에서 T*로의 표준 변환 시퀀스가 V2*에서 T*로의 것보다 낫다.

  4. 사용자 정의 변환 시퀀스 U1U2보다 낫다: 같은 생성자/사용자 정의 변환 함수를 호출하거나 집계 초기화로 같은 클래스를 초기화하고, 어느 경우든 U1의 두 번째 표준 변환 시퀀스가 U2의 두 번째 표준 변환 시퀀스보다 낫다.

  5. 목록 초기화 시퀀스 L1L2보다 낫다: L1std::initializer_list 매개변수를 초기화하는데 L2는 그렇지 않다.

  6. 목록 초기화 시퀀스 L1L2보다 낫다: 대응 매개변수가 배열에 대한 참조이고, L1이 "N1 T의 배열" 타입으로, L2가 "N2 T의 배열" 타입으로 변환하며, N1이 N2보다 작다. (C++11부터, C++20까지)

  7. 목록 초기화 시퀀스 L1L2보다 낫다: 대응 매개변수가 배열에 대한 참조이고, L1L2가 같은 요소 타입의 배열로 변환하며, L1이 초기화하는 요소 수 N1이 L2가 초기화하는 요소 수 N2보다 적거나, N1 == N2이고 L2는 경계 미지정 배열로 변환하는데 L1은 그렇지 않다. (C++20부터)

void f(int    (&&)[] ); // overload #1
void f(double (&&)[] ); // overload #2
void f(int    (&&)[2]); // overload #3
f({1});        // #1: Better than #2 due to conversion, better than #3 due to bounds
f({1.0});      // #2: double -> double is better than double -> int
f({1.0, 2.0}); // #2: double -> double is better than double -> int
f({1, 2});     // #3: -> int[2] is better than -> int[], 
               //     and int -> int is better than int -> double

두 변환 시퀀스가 같은 순위라서 구분할 수 없으면 다음 추가 규칙이 적용돼요.

  1. 포인터→bool 또는 멤버 포인터→bool을 포함하지 않는 변환이 포함하는 변환보다 낫다.
  2. (C++11부터) 고정 기반 타입을 가진 열거 타입을 그 기반 타입으로 승격하는 변환이, 승격된 기반 타입으로 승격하는 변환보다 낫다(두 타입이 다를 때).
  3. (C++23부터) 부동소수점 타입 FP1FP2 사이 한 방향 변환이 FP1과 산술 타입 T3 사이 같은 방향 변환보다 낫다.
  4. 파생→기반 포인터 변환이 파생→void 포인터 변환보다 낫고, 기반→void 포인터 변환이 파생→void 포인터 변환보다 낫다.
  5. MidBase에서 (직간접) 파생되고 DerivedMid에서 파생되면, Derived*Mid*Derived*Base*보다 낫고, DerivedMid&/Mid&&DerivedBase&/Base&&보다 낫고, Base::*Mid::*Base::*Derived::*보다 낫고, DerivedMidDerivedBase보다 낫고, Mid*Base*Derived*Base*보다 낫고, MidBase&/Base&&DerivedBase&/Base&&보다 낫고, Mid::*Derived::*Base::*Derived::*보다 낫고, MidBaseDerivedBase보다 낫다.

모호한 변환 시퀀스는 사용자 정의 변환 시퀀스로 순위가 매겨져요. 한 인자에 대한 여러 변환 시퀀스는 서로 다른 사용자 정의 변환을 포함할 때만 존재할 수 있기 때문이에요.

class B;

class A { A (B&);};         // converting constructor
class B { operator A (); }; // user-defined conversion function
class C { C (B&); };        // converting constructor

void f(A) {} // overload #1
void f(C) {} // overload #2

B b;
f(b); // B -> A via ctor or B -> A via function (ambiguous conversion)
      // b -> C via ctor (user-defined conversion)
      // the conversions for overload #1 and for overload #2
      // are indistinguishable; compilation fails

목록 초기화의 암시적 변환 시퀀스

목록 초기화에서 인자는 표현식이 아닌 brace-init-list예요. 그래서 오버로드 해석의 목적으로 매개변수 타입으로의 암시적 변환 시퀀스는 다음 특별 규칙으로 결정돼요.

  • 매개변수 타입이 어떤 집계 X이고 initializer-list가 같은 타입이나 파생 클래스(가능한 cv 제한)의 정확히 한 요소로 이뤄졌으면, 암시적 변환 시퀀스는 그 요소를 매개변수 타입으로 변환하는 데 필요한 것이에요.
  • 그 외에 매개변수 타입이 문자 배열에 대한 참조이고 initializer-list에 적절히 타입화된 문자열 리터럴이 단일 요소로 있으면, 암시적 변환 시퀀스는 identity 변환이에요.
  • 그 외에 매개변수 타입이 std::initializer_list<X>이고 initializer-list의 모든 요소에서 X로의 축소 없는(non-narrowing) 암시적 변환이 있으면, 오버로드 해석을 위한 암시적 변환 시퀀스는 필요한 최악의 변환이에요. brace-init-list가 비어 있으면 변환 시퀀스는 identity 변환이에요.
struct A
{
    A(std::initializer_list<double>);          // #1
    A(std::initializer_list<complex<double>>); // #2
    A(std::initializer_list<std::string>);     // #3
};
A a{1.0, 2.0};     // selects #1 (rvalue double -> double: identity conv)

void g(A);
g({"foo", "bar"}); // selects #3 (lvalue const char[4] -> std::string: user-def conv)
  • 그 외에 매개변수 타입이 "N T의 배열"(배열에 대한 참조에서만 발생)이면, initializer-list는 N개 이하의 요소를 가져야 하고, 리스트의 모든 요소(리스트가 N보다 짧으면 빈 중괄호 쌍 {})를 T로 변환하는 데 필요한 최악의 암시적 변환이 쓰여요.
  • (C++20부터) 그 외에 매개변수 타입이 "경계 미지정 배열의 T"(배열에 대한 참조에서만 발생)이면, 리스트의 모든 요소를 T로 변환하는 데 필요한 최악의 암시적 변환이 쓰여요.
typedef int IA[3];

void h(const IA&);
void g(int (&&)[]);

h({1, 2, 3}); // int->int identity conversion
g({1, 2, 3}); // same as above since C++20
  • 그 외에 매개변수 타입이 비-집계 클래스 타입 X면, 오버로드 해석이 인자 initializer-list에서 초기화할 X의 생성자 C를 골라요.
    • C가 initializer-list 생성자가 아니고 initializer-list에 (가능한 cv 제한의) X 단일 요소가 있으면 암시적 변환 시퀀스는 Exact Match 순위예요. (가능한 cv 제한의) X에서 파생된 타입의 단일 요소가 있으면 Conversion 순위예요. (집계와의 차이에 주목: 집계는 단일 요소 init-list에서 집계 초기화를 고려하기 전에 직접 초기화하고, 비-집계는 다른 생성자보다 initializer_list 생성자를 먼저 고려해요.)
    • 그 외에는 암시적 변환 시퀀스는 두 번째 표준 변환 시퀀스가 identity 변환인 사용자 정의 변환 시퀀스예요.
  • 여러 생성자가 유효한데 어느 것도 다른 것보다 낫지 않으면, 암시적 변환 시퀀스는 모호한 변환 시퀀스예요.
  • 그 외에 매개변수 타입이 집계 초기화에 따라 initializer-list에서 초기화할 수 있는 집계면, 암시적 변환 시퀀스는 두 번째 표준 변환 시퀀스가 identity 변환인 사용자 정의 변환 시퀀스예요.
  • 그 외에 매개변수가 참조면 참조 초기화 규칙이 적용돼요.
  • 그 외에 매개변수 타입이 클래스가 아니고 initializer-list에 요소가 하나 있으면, 암시적 변환 시퀀스는 그 요소를 매개변수 타입으로 변환하는 데 필요한 것이에요.
  • 그 외에 매개변수 타입이 클래스 타입이 아니고 initializer-list에 요소가 없으면, 암시적 변환 시퀀스는 identity 변환이에요.
인자가 지정 초기화자(designated initializer) 목록이고 매개변수가 참조가 아니면, 매개변수가 집계 초기화 규칙에 따라 그 initializer-list에서 초기화할 수 있는 집계 타입일 때만 변환이 가능하고, 이 경우 암시적 변환 시퀀스는 두 번째 표준 변환 시퀀스가 identity 변환인 사용자 정의 변환 시퀀스예요.
오버로드 해석 후 선택된 오버로드에서 집계 멤버의 선언 순서가 일치하지 않으면, 매개변수 초기화는 잘못된 형태가 돼요.
<br>struct A { int x, y; };<br>struct B { int y, x; };<br>void f(A a, int); // #1<br>void f(B b, ...); // #2<br>void g(A a); // #3<br>void g(B b); // #4<br>void h() <br>{<br> f({.x = 1, .y = 2}, 0); // OK; calls #1<br> f({.y = 2, .x = 1}, 0); // error: selects #1, initialization of a fails<br> // due to non-matching member order<br> g({.x = 1, .y = 2}); // error: ambiguous between #3 and #4<br>}<br>
(C++20부터)

결함 보고(Defect reports)

이전에 발표된 C++ 표준들에 소급 적용된 동작 변경 결함 보고들이 있어요.

DR 적용 대상 발표 당시 동작 올바른 동작
CWG 1 C++98 서로 다른 스코프에서 (가능하면 다른) 기본 인자를 가진 같은 함수가 선택될 때 동작이 불명확했음 이 경우 프로그램은 잘못된 형태
CWG 83 C++98 문자열 리터럴→char* 변환 시퀀스가 →const char*보다 나았음(전자는 폐기됐음에도) 폐기 변환의 순위가 낮아짐(C++11에서 제거)
CWG 162 C++98 &F(args) 경우 F가 가리키는 오버로드 집합에 비정적 멤버 함수가 있으면 유효하지 않았음 이 경우 오버로드 해석이 비정적 멤버 함수를 고를 때만 유효하지 않음
CWG 233 C++98 사용자 정의 변환이 있는 오버로드 해석에서 참조와 포인터가 비일관하게 다뤄졌음 일관되게 다뤄짐
CWG 280 C++98 접근 불가 기반 클래스에 선언된 변환 함수의 대리 호출 함수가 후보 함수 집합에 추가되지 않았음 접근성 제약 제거, 대리 호출 함수가 선택되고 대응 변환 함수를 호출할 수 없으면 프로그램은 잘못된 형태
CWG 415 C++98 함수 템플릿이 후보로 선택되면 그 특수화들이 템플릿 인자 추론으로 인스턴스화됐음 이 경우 인스턴스화는 일어나지 않고 선언이 합성됨
CWG 495 C++98 인자 암시적 변환이 동등하게 좋으면 비템플릿 변환 함수가 변환 함수 템플릿보다 항상 나았음(후자가 더 나은 표준 변환 시퀀스를 가질 수 있어도) 표준 변환 시퀀스가 특수화 수준보다 먼저 비교됨
CWG 1307 C++11 배열 크기에 기반한 오버로드 해석이 명시되지 않았음 가능하면 더 짧은 배열이 나음
CWG 1328 C++11 변환 결과에 참조를 바인딩할 때 후보 함수 결정이 명확하지 않았음 명확해짐
CWG 1374 C++98 표준 변환 시퀀스 비교 시 참조 바인딩 전에 자격 변환을 검사했음 순서 뒤집힘
CWG 1385 C++11 참조 제한자로 선언된 비-explicit 사용자 정의 변환 함수에 대응하는 대리 함수가 없었음 대응하는 대리 함수가 있음
CWG 1467 C++11 집계와 배열의 같은 타입 목록 초기화가 누락됐음 초기화가 정의됨
CWG 1601 C++11 열거 타입에서 기반 타입으로의 변환이 고정 기반 타입을 선호하지 않았음 고정 타입이 그것이 승격하는 타입보다 선호됨
CWG 1608 C++98 인자 타입이 T1인 단항 연산자 @의 멤버 후보 집합이 T1이 현재 정의 중인 클래스면 비어 있었음 이 경우 집합은 T1::operator@의 한정 이름 룩업 결과
CWG 1687 C++98 내장 후보가 오버로드 해석에 선택되면 피연산자가 제약 없이 변환을 겪었음 클래스 타입 피연산자만 변환하고 두 번째 표준 변환 시퀀스 비활성화
CWG 2052 C++98 잘못된 형태의 합성 함수 템플릿 특수화가 후보 집합에 추가되어 프로그램을 잘못된 형태로 만들 수 있었음 후보 집합에 추가되지 않음
CWG 2076 C++11 목록 초기화 중 중첩 initializer-list의 단일 초기화자에 사용자 정의 변환이 적용됐음(CWG 1467 해결 때문) 적용되지 않음
CWG 2137 C++11 {X}에서 X를 목록 초기화할 때 initializer-list 생성자가 복사 생성자에게 졌음 비-집계는 initializer list를 먼저 고려함
CWG 2273 C++11 상속된 생성자와 비상속 생성자 사이에 타이브레이커가 없었음 비상속 생성자가 이김
CWG 2673 C++20 재작성 비멤버 후보와 같은 매개변수 목록을 가진 내장 후보가 내장 후보 목록에 추가됐음 추가되지 않음
CWG 2712 C++98 내장 대입 연산자를 고려할 때 첫 번째 매개변수가 임시 객체에 바인딩될 수 없다는, 이미 불가능한 요구가 있었음 중복 요구 제거
CWG 2713 C++20 지정 초기화자 목록에 관한 변환 제한이 매개변수가 참조여도 적용됐음 이 경우 제한되지 않음
CWG 2789 C++23 매개변수 타입 목록 비교 시 명시적 객체 매개변수가 포함됐음 제외됨
CWG 2856 C++11 복사 목록 초기화 문맥의 기본 초기화에 대한 오버로드 해석이 변환 생성자만 고려했음 모든 생성자 고려
CWG 2919 C++98 변환에 의한 참조 초기화의 후보 집합이 초기화의 대상 타입에 의존했음 변환의 대상 타입에 의존
P2468R2 C++20 일치하는 operator!=가 있어도 a != boperator== 기반 재작성 후보가 추가됐음 추가되지 않음

더 알아보기