MSVC LNK2019 Error
Visual Studio(MSVC) LNK2019 Error#
C/C++ 파라미터에서 배열 표기는 전부 가짜다.
int arr[3]이라고 써도 결국 int *arr로 조정된다. 이는 표준이 명시하는 사실이다. 그래서 나는 당연히 아래 두 함수가 완전히 같다고 생각했다. (미리 말하지만, 같은게 맞다.)
void process(int mCoordinates[][2]) // (1)
void process(int (*mCoordinates)[2]) // (2)
근데 Visual Studio에서 (2)로 쓰면 링크 에러가 나고, (1)로 바꾸면 된다. 같은 함수인데 왜?
실제로 있었던 문제점#
main.cpp:
extern void process(int co_bak[][2]);user.cpp:
void process(int (*mCoordinates)[2]) { ... }빌드하면 아래 에러가 떴다.
error LNK2019: unresolved external symbol
"void __cdecl process(int (* const)[2])" (?process@@YAXQEAY01H@Z)
referenced in function main두 가지가 눈에 띈다.
int (*)[2]가 아니라int (* const)[2]이다. -const가 붙어 있다.- mangling된 이름이
?process@@YAX**Q**EAY01H@Z-Q로 시작한다.
MSVC name mangling에서 P는 mutable pointer, Q는 top-level const pointer다. 즉 MSVC가 int co_bak[][2]를 int (* const)[2]로 mangling한 거다.
C++은 함수 오버로딩, 네임스페이스, 템플릿 같은 기능 때문에 같은 이름의 함수가 여러 개 존재할 수 있다. 그런데 링커는 결국 심볼 이름 하나로 함수를 식별해야 하기 때문에, 컴파일러는 함수 이름·파라미터 타입·반환 타입·네임스페이스 등을 인코딩해서 유일한 심볼 이름을 만들어낸다. 이걸 name mangling이라고 한다. g++/clang은 Itanium ABI를 따르는
_Z...형태, MSVC는 자체 규칙으로?...@@...형태다.
반면 정의 쪽 void process(int (*mCoordinates)[2])는 정상적으로 P로 mangling되어 ?process@@YAXPEAY01H@Z가 된다.
선언 (main.cpp) : ?process@@YAX Q EAY01H@Z
정의 (user.cpp) : ?process@@YAX P EAY01H@Z
↑
서로 다른 심볼링커 입장에서는 정의가 없는 함수를 호출하는 셈이고, LNK2019가 나는 거다.
실제 표준은?#
C++ 표준 [dcl.fct]/5는 함수 파라미터 타입을 두 가지 방식으로 조정한다.
- Array-to-pointer decay —
T[N]형태 파라미터는T*로 조정 - Top-level cv-qualifier 제거 — 파라미터 최상위
const/volatile은 함수 타입에서 제거
그래서 아래는 다 같은 함수 타입 void(int*)다.
void f(int a[10]);
void f(int a[]);
void f(int *a);
void f(int * const a); // top-level const는 무시됨
2차원 배열도 마찬가지고.
void f(int a[][2]); // → int(*)[2]
void f(int (*a)[2]); // → int(*)[2]
int a[][2]와 int (*a)[2]는 완전히 동일한 타입이어야 하고, mangling 결과도 당연히 같아야 한다.
왜 MSVC만 이럴까?#
MSVC는 오래전부터 배열 형태 파라미터를 decay할 때 암묵적으로 const를 붙이고, mangling 단계에서도 그걸 제거하지 않는다. 표준 비준수인데 ABI 안정성 때문에 고쳐지지 않고 그대로 남아있다고 한다.
g++, clang은 표준대로 동작하니까 두 형태가 같은 심볼 이름을 갖는다.
| 컴파일러 | int a[][2] |
int (*a)[2] |
같은 심볼? |
|---|---|---|---|
| g++ / clang | int(*)[2] |
int(*)[2] |
O |
| MSVC | int(* const)[2] |
int(*)[2] |
X |
해결 방법#
선언과 정의의 표기를 둘 중 하나로 통일하면 된다.
옵션 A — 양쪽 다 배열 형태
// main.cpp
extern void process(int co_bak[][2]);
// user.cpp
void process(int mCoordinates[][2]) { ... }옵션 B — 양쪽 다 포인터 형태
// main.cpp
extern void process(int (*co_bak)[2]);
// user.cpp
void process(int (*mCoordinates)[2]) { ... }Comment#
ABI 호환성 때문에 한 번 굳어진 quirk는 잘 안 고쳐진다는데, 아무튼 이래서 MSVC를 쓰기가 싫다.