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

두 가지가 눈에 띈다.

  1. int (*)[2]가 아니라 int (* const)[2]이다. - const가 붙어 있다.
  2. 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는 함수 파라미터 타입을 두 가지 방식으로 조정한다.

  1. Array-to-pointer decayT[N] 형태 파라미터는 T*로 조정
  2. 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를 쓰기가 싫다.