2021年11月21日日曜日

cppのvirtualなdestructorに対する「undefined reference to `vtable for クラス名::~クラス名`」を回避する書き方


背景

c++でvirtualを利用して継承元となるクラスを定義したところ、表題のように「undefined reference to `vtable for クラス名::関数`」というエラーが出てビルドに失敗しました。

初めて遭遇した時はデストラクタのみにそのエラーが出ていたため、defaultの代入により問題を回避しました。
しかしながら、「継承元のクラスでvirtualだけど自身の領域削除処理も行いたい場合」はどうするか疑問だったため、エラーを回避しつつデストラクタの処理を記述する方法を調べてみました。

備忘録を兼ねて書き方を記事に残します。

使ったもの

c++11の実行環境
記事の前半はUbuntu20.04にインストールしたg++-11を利用しています。

参考: Install g++ 11 on Ubuntu 20.04

後半はArduino環境を利用しています。
書き方によってエラーになったりならなかったり、デストラクタが問題でないのにそれが指摘される場合を解説します。

ビルドできる書き方1: virtualなデストラクタや関数の宣言に空の情報を代入する

継承先のデストラクタを呼びたいものの、継承元のデストラクタは空で良い場合に使える書き方です。
デストラクタにはdefaultを、関数には0を代入することで、定義が空と宣言できます。
#include <cstdio>

class Base {
public:
virtual ~Base() = default;
virtual void printName() = 0;
};

class TypeA : public Base {
public:
TypeA(const char* name) : name(name) {
printf("construct typeA with %s\n", name);
}

~TypeA() { printf("destruct TypeA\n"); }
void printName() { printf("Name is %s\n", name); }

private:
const char* name;
};

int main() {
Base* a1 = new TypeA("a1");
a1->printName();
delete a1;
return 0;
}

実行するとBaseのvirtual関数の呼び出しを通してTypeAのprintNameやデストラクタを呼び出せています。
construct typeA with a1
Name is a1
destruct TypeA


ビルドできる書き方2: virtualなデストラクタに実装を書き、関数は空の情報を代入する

継承先のデストラクタも、継承元のデストラクタも呼びたい場合に使える書き方です。
#include <cstdio>

class Base {
public:
Base() { printf("construct Base\n"); }
virtual ~Base() { printf("destruct Base\n"); };
virtual void printName() = 0;
};

class TypeA : public Base {
public:
TypeA(const char* name) : Base(), name(name) {
printf("construct typeA with %s\n", name);
}
~TypeA() { printf("destruct TypeA\n"); }
void printName() { printf("Name is %s\n", name); }

private:
const char* name;
};

int main() {
Base* a1 = new TypeA("a1");
a1->printName();
delete a1;
return 0;
}

実行するとBase関係が最初と最後に処理され、その間にTypeA関係が実行されているのが分かります。
construct Base
construct typeA with a1
Name is a1
destruct TypeA
destruct Base

余談: 環境によってvirtualな関数に何も入れなくてもビルドが通る場合があるが、virtualな関数をどれか実装すると全てのvirtualな関数の実装を求めてくることがある

この長い見出しは、今回の記事を書くきっかけとなった出来事です。
具体的には下記のavrとesp32のArduinoの環境で発生しました。
gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
xtensa-esp32-elf-g++ --version
xtensa-esp32-elf-g++ (crosstool-NG crosstool-ng-1.22.0-80-g6c4433a) 5.2.0
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

下記のコードならビルドに成功します。
virtualのデストラクタにdefaultを、関数は宣言のみで何も代入しない書き方です。
なお、この書き方だとc++11の環境では関数の宣言が無いとエラーが出てビルド出来ません。
class Base {
public:
virtual ~Base() = default;
virtual void printName();
};

class TypeA : public Base {
public:
TypeA(const char* name) : Base(), name(name) {
Serial.println("construct typeA with " + String(name));
}
~TypeA() { Serial.println("destruct TypeA"); }
void printName() { Serial.println("Name is " + String(name)); }

private:
const char* name;
};

void setup() { Serial.begin(115200); }

void loop() {
Base* a1 = new TypeA("a1");
a1->printName();
delete a1;
delay(2000);
}

上記の実装のBaseのデストラクタに実装を追加すると、なぜか該当するデストラクタの実装が無いというエラーでビルド出来なくなります。
class Base {
public:
virtual ~Base() { Serial.println("destruct Base"); }; // デストラクタを定義
virtual void printName();
};

下記がエラーの内容です。

avrの場合
/tmp/ccAn6Lwu.ltrans0.ltrans.o: In function `Base::~Base()':
<artificial>:(.text+0xcc6): undefined reference to `vtable for Base'

esp32の場合
.pio/build/esp32dev/src/main.cpp.o:(.literal._ZN5TypeAD5Ev[TypeA::~TypeA()]+0xc): undefined reference to `vtable for Base'
collect2: error: ld returned 1 exit status

エラーの指摘している箇所では無いですがvirtualな関数の宣言が空である記述(0の代入)をするとエラー無くビルドできるようになります。
class Base {
public:
virtual ~Base() { Serial.println("destruct Base"); };
virtual void printName() = 0; // 0を代入して空と宣言
};

コンパイラのエラーの解釈間違いなのかもしれません。

まとめ

「undefined reference to `vtable for クラス名::~クラス名`」というエラーは、デストラクタはdefault、関数は0を代入することで回避できました。
また、環境によってはvirtualなデストラクタを定義しているにも関わらずそれの定義が無いとエラーになることがありますが、該当するクラスで定義しているvirtualな関数に0を代入するとエラーが出なくなりました。

関数の定義が無くてエラーになるのにデストラクタの定義が無いとエラーが出てしまう環境は謎ですが、そうなって困らないように空のvirtualな関数には0を代入しておくと良さそうです。

参考

Undefined reference to vtable


0 件のコメント :