카테고리 보관물: Programming

C++에서 오버라이드된 함수가 안불러져요

Data라고 하는 정보를 저장하는 클래스들의 기초 클래스가 있다고 해보자. Data의 type에 따라 로딩 하는 방법이 서로 달라지기 때문에 loadData()라고 하는 가상함수를 하나 정의해 두고, 상속받는 클래스들은 자기가 loading하는 방법을 정의하도록 구성한다.

class Data {
public:
  Data(int v) { loadData(v); }
  void dispValue() { cout << "Value is " << _val << endl; }

protected:
  // 상속받는 클래스에서 구현해야 함.
  // 호출되면 안됨.
  virtual bool loadData(int v) {
    cout << "Default loader, do not call me!" << endl;
    return false;
  }
  int _val = 0;
};

Data에서 상속받는 DerivedData 클래스는 자기 버전의 loadData()를 오버라이드해서 구현한다.

class DerivedData : public Data {
public:
  DerivedData(int v) : Data(v) {}

protected:
  bool loadData(int v) override {
    cout << "DerivedData1::loadData()" << endl;
    _val = v;
    return true;
  }
};

DerivedData를 생성하고 100으로 초기화해서 결과를 보여주도록 main()을 작성한다.

int main(void) {
  auto d = DerivedData(100);
  d.dispValue();
}

이제 컴파일하고 실행해보자.

$ g++ ./main.cpp 
$ ./a.out 
Default loader, do not call me!
Value is 0

예상과 달리 DerivedData의 loadData()가 아닌 Data의 loadData()가 불리고 있다. 분명히 DevriedData의 인스턴스를 만들었는데도 오버라이드한 멤버함수가 동작하지 않는 이유는 무엇일까?

이 예제에서는 Data에서 상속 받은 DerivedData 클래스의 생성자가 호출되기 전에 Data의 생성자가 먼저 호출된다. Data클래스의 생성자에서는 가상함수로 정의된 loadData()의 오버라이드 구조를 따라 내려가지 않고 현재 클래스인 Data에 정의되어 있는 loadData() 를 바인딩한다. 아직 DerrivedData 클래스가 초기화 되지 않은 시점이기 때문에 기초 클래스의 생성자에서 파생 클래스의 오버라이드 멤버를 바인딩하는 것은 아직 알지 못하는 정보를 참조해야 하는 일이되기 때문이다.

Scott Meyers의 명서인 Effective C++에서도 이 내용을 언급하고 있었다. “Item 9: Never call virtual functions during construction or destruction”(생성자나 소멸자에서 가상함수를 절대 호출하지 말것) 아마도 다시 정독할 때가 됐나 보다.

해결책으로 제시되는 것은 가상 함수를 부르는 대신 관련한 정보를 파라미터로 받아서 처리하는 것이다. 위의 경우 예를 들면 loadData()를 일반 멤버함수로 작성하고 DataLoader를 파라미터로 넘겨주는 방법을 고려해 볼수 있다.

#include <iostream>
using namespace std;

class DataLoader {
public:
  DataLoader(int v) { _val = v; }
  int getValue() { return _val; }

private:
  int _val;
};

class Data {
public:
  Data(DataLoader &ldr) { loadData(ldr); }
  void dispValue() { cout << "Value is " << _val << endl; }

protected:
  bool loadData(DataLoader &ldr) {
    cout << "Load data " << ldr.getValue() << endl;
    _val = ldr.getValue();
    return true;
  }
  int _val = 0;
};

class DerivedData : public Data {
public:
  DerivedData(DataLoader &ldr) : Data(ldr) {}
};

int main(void) {
  auto l = DataLoader(100);
  auto d = DerivedData(l);
  d.dispValue();
}
$ ./a.out 
Load data 100
Value is 100

C++ template 선언과 구현을 서로 다른 파일에 나눌 수 없다

커스텀 Queue 클래스를 작성하는데 queue의 item은 추후에 변경 될 수 있으므로 템플릿으로 작성하고자 한다. Queue class를 header에 정의하고 구현을 cpp 파일에 작성해 준다음 main 함수에서 다음과 같이 queue를 생성하고 컴파일을 시도한다.

다음은 해당 template을 사용하는 caller(main 함수)이다.

//
// C++ template instantiation test.
//
//                                             - litcoder

#include <iostream>
#include "queue.h"

using namespace std;
int main() {
    // Create an integer type queue.
    queue<int> q;

    // Push elements from the queue.
    q.push(0);
    q.push(1);
    q.push(2);

    // Pop and print all elements.
    while (! q.empty()) {
        cout << "Popped: " << q.pop() << endl;
    }

    return 0;
}

하지만 별 문제 없을것이라는 생각과 달리 안타깝게도 빌드는 실패한다. 공들여서 만든 method가 링커에 의해 하나도 빠짐없이 “undefined reference” 오류를 뱉으면서 실패한다.

$ make
g++  -g -Wall  -c -o queue.o queue.cpp
g++  -g -Wall  -c -o main.o main.cpp
g++ -o qtest queue.o main.o
/usr/bin/ld: main.o: in function `main':
/home/litcoder/Downloads/cpptemplate/main.cpp:12: undefined reference to `queue<int>::queue()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:15: undefined reference to `queue<int>::push(int)'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:16: undefined reference to `queue<int>::push(int)'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:17: undefined reference to `queue<int>::push(int)'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:21: undefined reference to `queue<int>::pop()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:20: undefined reference to `queue<int>::empty()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:25: undefined reference to `queue<int>::~queue()'
/usr/bin/ld: /home/litcoder/cpptemplate/main.cpp:25: undefined reference to `queue<int>::~queue()'
collect2: error: ld returned 1 exit status
make: *** [Makefile:7: qtest] Error 1

이 문제에 대한 해결책은 생각보다 간단한데, cpp파일에 따로 분리했던 구현 부분을 선언들과 함께 header file에 두는 것이다. (해치웠나?)

$ make
g++  -g -Wall  -c -o queue.o queue.cpp
g++  -g -Wall  -c -o main.o main.cpp
g++ -o qtest queue.o main.o
$ ./qtest
Popped: 0
Popped: 1
Popped: 2

왜 undefined reference 오류가 났을까?

Queue.o가 빌드됐으니 거기 보면 분명히 저 method들이 선언되어 있을텐데 말이다. 빌드된 symbol들을 살펴보자,

$ objdump -t ./queue.o

./queue.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 queue.cpp

O.O 없다. 아무것도. queue.o는 아무 symbol이 없는 빈 파일이었다.

앞서 해결됐다고 했던 header에서 template 선언과 구현을 모두 수행하는 경우에도 queue.o의 symbol table이 비어 있는것은 마찬가지 이지만, 이 경우, caller인 main.o에는 template이 실제 사용하는 type에 binding되어 embed되어 있다. main.o의 symbol들을 살펴보면 다음과 같이 mangle된 symbol들을 볼 수 있다.

$ objdump -t ./main.o

./main.o:     file format elf64-x86-64

SYMBOL TABLE:
0000000000000000 l    df *ABS*	0000000000000000 main.cpp
...
0000000000000000  w    F .text._ZN5queueIiEC2Ev	0000000000000048 _ZN5queueIiEC1Ev
0000000000000000  w    F .text._ZN5queueIiE4pushEi	000000000000003f _ZN5queueIiE4pushEi
...

참고로, C++ symbol의 type은 demangler를 이용하면 볼 수 있는데, demangle한 결과는 다음과 같다.

_ZN5queueIiEC2Ev -> queue<int>::queue()
_ZN5queueIiE4pushEi -> queue<int>::push(int)

결론

C++ template은 빌드하는 시점에서 어떤 data type으로 binding될 지 알 수 없기 때문에 구현 부분을 별도의 파일로 만들어 봤자 아무 것도 빌드 되지 않는다. Header file에 선언과 구현을 함께 두고 이 template을 사용하는 code가 이를 include하여 어떤 data type으로 binding될 지 결정해 주면 그때서야 실제 symbol이 caller module에 포함된다.

즉, template을 선언과 구현으로 나누어서 따로 빌드하는 것은 불가능할 뿐더러 의미도 없으니 include될 하나의 파일(주로 header)에 넣어서 선언과 구현을 합쳐두는게 가장 좋다.

Ubuntu 22.04 Linux kernel 6.1.1로 올리기

Ubuntu 22.04에서 Linux kernel 6.1.1 빌드해서 설정한 내용 정리이다. 굳이 실험정신을 억누르지 못하는게 아니라면 간단히 deb package를 다운로드 받아서 설치해도 된다.

Prerequisites

컴파일에 필요한 tool들을 설치한다.

sudo apt-get install libncurses-dev gawk flex bison openssl libssl-dev dkms libelf-dev libudev-dev libpci-dev libiberty-dev autoconf llvm

Linux kernel code를 다운로드 받고 합축을 해제해 둔다.

wget -c https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.1.tar.xz && tar xvf ./linux-6.1.1.tar.xz

Kernel config 설정 및 build 실행

Linux kernel config를 하나하나 설정하려면 시간이 오래 걸리니 현재 잘 동작하는 있는 kernel 설정 파일을 가져와서 설정하자.

cd ./linux-6.1.1
cp /boot/config-$(uname -r) ./.config

fakeroot debian/rules clean
make olddefconfig

추가로, 다음과 같이 복사된 .config를 열어서 KEY 설정을 주석으로 없애주는게 좋다. 그렇지 않으면 “No rule to make target ‘debian/canonical-certs.pem’, needed by ‘certs/x509_certificate_list’. Stop.” 오류가 발생하면서 빌드가 멈출 것이다.

#                                                                                                   
# Certificates for signature checking                                                               
#                                                                                                   
...                                                              
CONFIG_SYSTEM_TRUSTED_KEYS="" #COMMENT OUT "debian/canonical-certs.pem"                                              
...                                                            
CONFIG_SYSTEM_REVOCATION_KEYS="" #COMMENET OUT "debian/canonical-revoked-certs.pem"   

모든 설정이 완료되면 빌드를 실행한다.

fakeroot debian/rules binary -j$(nproc)

Build된 패키지 설치 및 확인

빌드가 완료되면 다음과 같이 4개의 deb file들이 source code의 상위 디렉토리에 생성되었는지를 확인하고 apt 명령어로 설치 후 system을 reboot한다.

ls ../*.deb
  ../linux-headers-6.1.1_6.1.1-2_amd64.deb
  ../linux-image-6.1.1-dbg_6.1.1-2_amd64.deb
  ../linux-image-6.1.1_6.1.1-2_amd64.deb
  ../linux-libc-dev_6.1.1-2_amd64.deb

sudo apt install ../*.deb
sudo reboot

BIOS로 진입해서 secure boot을 disable하고 GRUB에서 새로 설치한 kernel을 선택해서 부팅한 후 다음과 같이 확인할 수 있다.

uname -a
Linux <machine-name> 6.1.1 #2 SMP PREEMPT_DYNAMIC Thu Dec 29 13:15:23 KST 2022 x86_64 x86_64 x86_64 GNU/Linux