1. 스마트포인터를 인식하지 못하는 버그

회사에서 이미 작성된 C++ 클래스 멤버 함수를 사용하고 싶어서 호출을 했는데.. 컴파일러인지 링커인지가 도무지 말귀를 알아듣질 못하고 unreferenced external symbol 링크 에러를 내뱉곤 했다. 매크로 치환, namespace 그 어떤 문제도 없는데 왜?
더 골때리는 건.. 같은 코드가 Windows에서 Visual C++은 아무 문제 없이 빌드되고, 안드로이드의 NDK 빌드 환경에서만 저런다는 것이었다.

그 함수는 첫째 인자의 타입이 FOO const&이었는데, FOO는 스마트 포인터 std::shared_ptr<BAR>의 typedef였다.
스마트 포인터를 왜 value로 전달하지 않고 또 레퍼런스로 전달했는지, 그 이유는 모르겠다. 이 코드를 처음에 내가 작성한 게 아니니까..

그런데 문제는 저 스마트 포인터를 그냥 날포인터 BAR*로 바꿔 주니까 링크 에러 없이 빌드가 됐으며, 프로그램도 양 플랫폼 다 별 문제 없이 돌아가기 시작했다는 것이다.
어느 경우건 -> 연산자를 쓰면 BAR 내용을 참조할 수 있으며, 몇몇 곳에서만 ptr 대신에 ptr.get()을 호출해 주면 됐다.

결국 이 문제의 원인은 안드로이드 쪽의 컴파일러 내지 링커의 버그이긴 한 것 같다. 하나만 고르라면 링커보다도 컴파일러의 문제인지도? 복잡한 type의 decoration string가 양쪽에서 서로 동일하게 생성되지 못했던 것으로 보인다.

2. 변수에도 extern "C" 구분이 필요한가

C++ 코드에서 다른 C 소스 파일에 정의된(C 소스로부터 빌드된 obj, lib도 포함) 함수를 참조해서 호출하려면.. 그 함수의 prototype이 extern "C" 형태로 선언되어야 한다.
C++은 오버로딩이라는 게 존재하기 때문에 C와 달리 함수를 이름만으로 유일하게 식별할 수 없으며, 인자들의 개수와 타입들도 명칭 decoration에 다 들어가야 하기 때문이다.

이건 상식 중의 상식이다. 그렇기 때문에 C언어 방식으로 만들어진 라이브러리는 헤더 파일이 중복 include guard뿐만 아니라

#ifdef __cplusplus
extern "C" {
#endif

(.....)

#ifdef __cplusplus
}
#endif

이렇게 관례적으로 감싸져 있기도 하다. C++ 코드에서 인클루드 되더라도 여기 함수들은 C++이 아닌 C 방식으로 링크 하라고 말이다.

그런데.. 난 함수뿐만 아니라 전역 변수도 이런 decoration 방식이 차이가 존재하며, 서로 일치해야 한다는 걸 요 근래에야 처음으로 알게 됐다.
C++이 C 코드에서 선언된 전역 변수를 참조하려면.. 역시 extern "C" int Global_in_C_code; 이렇게 해 줘야 된다. extern "C"를 생략하면 링크 에러가 난다..;;

헐 왜 그렇지..?? 변수는 언어 문법 차원에서 decoration이 전혀 필요해 보이지 않는데..?? Visual C++만 그런가?

그러고 보니 Visual C++은 함수를 C++ 형태로 decoration을 할 때 인자뿐만 아니라 리턴 타입까지 그 함수의 prototype의 모든 정보를 써 넣는다.
함수의 리턴 타입은 오버로딩 변별 요소가 아니기 때문에 "굳이 써 넣을 필요가 없음에도 불구하고" 그리한다는 것이다.

그런 것처럼 그냥 completeness 차원에서.. 나중에 미래에 혹시 필요할지도 모르니까 변수도 C++ 방식에서는 자신의 type까지 다 꼼꼼히 써 넣는 게 아닐까? 나로서는 이렇게밖에 생각되지 않는다.
예전에 C++에서는 const 전역 변수는 반드시 extern을 명시해 줘야 다른 번역 단위에서도 참조 가능해진다는 걸 알지 못해서 오랫동안 컴파일러/링커의 난독증을 의심하며 짜증 냈던 적이 있었는데.. 이것도 좀 비슷한 상황인 것 같다.

심지어 extern "C" 다음에 { }를 쳐서 C 방식의 외부 전역 변수 선언을 여러 개 하려면 중괄호 안에다가 extern을 또 써 줘야 된다. extern "C" { extern int x,y,z; } 처럼.

extern "C" { int x,y,z; }
이렇게 하면 x,y,z가 이 번역 단위 안에서 몸체가 직접 정의돼 버린다. 그렇기 때문에 unresolved symbol 대신, 명칭 중복 선언 충돌이라는 링크 에러가 날 수 있게 된다.

즉, 선언만 하고 마는 것은 중괄호와 함께 extern을 또 명시한 extern "C" { extern int x,y,z; } 이거 아니면..
그냥 extern "C" int x,y,z; 둘 중 한 형태라는 것이다. 어휴~ ㄲㄲㄲㄲ

3. 에러 안내

(1) 컴파일 에러는 컴파일러가 지적해 준 부분의 주변만 유심히 살펴보면 대체로 쉽게 해결 가능하다. 아주 복잡하게 꼬인 템플릿 코드에서 컴파일러가 뜬구름 잡는 난해한 소리만 늘어놓는다면 그건 상황이 다르지만, 그 정도로 극단적인 상황은 흔치 않다.
그 반면, 컴파일 에러보다 훨씬 더 무질서도가 높고 난해한 에러는 링커 에러일 것이다.

요즘 컴파일러는 명칭의 오타 때문에 에러가 나면 근처의 스펠링이 비슷한 변수· 함수를 제안까지 하면서 "혹시 이걸 의도하셨습니까?" / "혹시 뒤에 세미콜론을 빠뜨렸습니까?" 이런 안내를 할 정도로 똑똑해졌다.
링커도 "동일한 명칭이 C 방식으로는 존재하는데 혹시 extern "C"를 빠뜨렸습니까?" 정도의 유사 명칭 안내는 해 줘야 하지 않나 싶다.

(2) 아 하긴, C++ 템플릿은 그 자체만으로는 컴파일러가 문법 검사를 전혀 하지 않으며, 그 구조상 할 수도 없다.
템플릿에 인자가 주어져서 어떤 타입에 대한 실체가 생겼을 때에만 컴파일러가 그에 대한 코드를 생성할 수 있으며, 이때 비로소 문법 검사가 행해진다.

템플릿과 관련해서 발생하는 컴파일 에러는 뭔가.. 한 박자 다음에 발생한다는 점으로 인해 링커 에러처럼 더욱 난해한 구석이 있다.
템플릿 인자가 그 어떤 형태로 주어지더라도 무조건 발생할 수밖에 없는 컴파일 에러는 템플릿 자체의 코드만 보고도 컴파일러가 먼저 딱 잡아낼 수도 있으면 좋겠다만.. C++ 컴파일러 업계에서 그런 건 아직 신경을 안 쓰는가 보다. 메타프로그래밍이란 건 아무래도 추상화 수준이 높고 매우 난해한 기술이기도 하니 말이다.

4. 버전이 올라가면서 달라지는 C++ 컴파일러 동작

cmake라고 플랫폼별로 파편화돼 있는 개발툴 프로젝트/빌드 스크립트를 한데 통합해 주는 프로그램이 있다.
이건 분명 현실에서의 난해하고 복잡한 문제를 단순화시키고 해결하기 위해 만들어진 도구이겠지만.. 본인은 오픈소스나 크로스 플랫폼 같은 쪽으로는 인연이나 경험이 없다시피한 Windows 토박이에 Visual Studio 매니아이다 보니 얘를 다루는 게 참 난감하고 버겁게 느껴졌다.

회사에서 굉장한 구닥다리인 Visual Studio 2013을 오랫동안 쓰고 있어서 이걸 2019로 올리고, 플랫폼도 x86뿐만 아니라 x64도 추가하고 싶은데.. 그러려면 cmake 스크립트를 어떻게 바꿔야 하는지 알 길이 없었다.

나중에 알고 보니 cmake 자체도 버전업을 해야 했다. 그런데 VS가 2013이 없고 2019만 있을 때 발생하는 에러 메시지들이 그 근본 원인과는 전혀 관계 없는 엉뚱한 것들이어서 에러 메시지가 짚어 주는 부분만 뒤져서는 문제의 원인을 도무지 알 수 없었다.

cmake 따위 없이 Visual Studio 솔루션과 프로젝트 파일만 있었으면 이건 뭐 일도 아니었을 텐데 이런 것들이 cmake 스크립트가 좀 유연하지 못한 구석이 있는 것 같았다. 특정 Visual Studio 버전과 특정 타겟 아키텍처에 매인 비중이 크다. 뭐, 사실은 본인이 cmake 사용법을 잘 몰라서 삽질하는 것이겠지만..
cmake나 git 같은 빌드 관련 툴들은 학교에서 가르치기에는 너무 남사스럽고, 학원도 아니고.. 천상 스스로 독학하거나 직장에서 알음알음 배우는 수밖에 없나 모르겠다.

그리고 이렇게 컴파일러를 업글 하고 나면.. 기존 코드가 자잘하게 컴파일이 안 되는 부분이 꼭 발생하곤 한다. 그런 건 내 경험상.. C++이 갈수록 type safety가 강화되어서 더 까칠 엄격해지기 때문인 것 같다.
직장에서의 경험을 회고해 보자면, 이 클래스가 이 상태로는 vector, list, set 같은 컨테이너에 들어가지 않아서 에러가 나곤 했다. 2013에서는 됐는데 2019에서는 안 되는 것이다.

operator =의 인자가 T였던 것을 const T&로 바꾸고, 복사 생성자가 정의돼 있지 않던 것을 명시적으로 넣어 주고, 원래는 생성자에다가 U라는 타입 값을 넣으면 자동으로 형변환이 됐는데 이제는 되지 않아서 명시적으로 형변환을 하는 등.. 에러를 해결하는 방식이 다들 이런 식이었다.

Posted by 사무엘

2022/07/08 08:35 2022/07/08 08:35
,
Response
No Trackback , No Comment
RSS :
http://moogi.new21.org/tc/rss/response/2040


블로그 이미지

철도를 명절 때에나 떠오르는 4대 교통수단 중 하나로만 아는 것은, 예수님을 사대성인· 성인군자 중 하나로만 아는 것과 같다.

- 사무엘

Archives

Authors

  1. 사무엘

Calendar

«   2022/07   »
          1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31            

Site Stats

Total hits:
1952889
Today:
655
Yesterday:
781