LoginSignup
1
0

More than 1 year has passed since last update.

Go言語のinterfaceっぽいC++のクラスを自動生成するツールを作った話

Last updated at Posted at 2021-12-23

初投稿記事です。
[12/28追記]
N髙生です。ちまちまとライブラリを書いています。
N髙アドベントカレンダー19日目の記事です。

結論

・golangのinterfaceみたいなのをc++で自動生成するツール作ったよ
・よい子のみんなは普通にgolang使おうね

動機

http通信のライブラリを書いていた
tcpとかsslとかを動的に切り替えたい
継承とかは使いたくない。
なんなら、他人の作った他のプロトコルでも好きにすげ替えたい。需要あるのか?
golangを勉強する -> interfaceめっちゃ便利
C++でも似たようなことしたい -> Type Erasureでできるな。
一応できた -> めんどくさいしDRY原則に反するぜ。
そうだ、自動生成ツールを作ろう!

前提:C++におけるType Erasureとは

テンプレートと継承を使って条件を満たす任意の型の値を動的に保持する技法で
標準ライブラリではstd::functionとかstd::anyとかstd::shared_ptrとかで使われている。
下のコードは実装の一例である。
コメントに注意しつつ読んでもらいたい。

Hey.cpp
#include <string>
#include <utility>
#include <functional>
#include <iostream>

struct SayHey {
   private:
    struct interface {
        virtual std::string hey() = 0; //hey 1回目

        ~interface() {}
    };

    template <class T>
    struct implements : interface {
        T t;
        implements(T&& t)
            : t(std::move(t)) {}

        std::string hey() override { //hey 2回目
            return t.hey(); //hey 3回目
        }
    };

    interface* iface = nullptr;

   public:
    constexpr SayHey() {}

    template <class T>
    SayHey(T&& t) {
        iface = new implements<T>(std::move(t));
    }

    SayHey(SayHey&& in) noexcept {
        iface = in.iface;
        in.iface = nullptr;
    }

    SayHey& operator=(SayHey&& in) noexcept {
        delete iface;
        iface = in.iface;
        in.iface = nullptr;
        return *this;
    }

    std::string hey() { //hey 4回目
        if (!iface) {
            throw std::bad_function_call();
        }
        return iface->hey(); //hey 5回目
    }
};

struct Human {
    std::string hey() {
        return "Hey!";
    }
};

struct Dog {
    std::string hey() {
        return "Bow Wow";
    }
};

int main() {
    SayHey hey;

    hey = Human{};
    std::cout << hey.hey() << "\n";

    hey = Dog{};
    std::cout << hey.hey() << "\n";
}

そう、たかが関数1個のために、5回も同じようなことを書かなければならない。
さらには、コンストラクタとかデストラクタとか代入演算子とかも
きちんと書かなきゃいけない。スマートポインタ使え
上の例は引数がないが、引数があったり関数の数が増えたりすれば
繰り返し繰り返し同じようなコードを書く事になる。
間違える事必至だ。さらに、関数名を変えるときの事を考えてほしい。
絶対にいやになるだろう。エディタの置換機能使え

対してGo言語のinterfaceはというと、

Hey.go
package main

import "fmt"

type SayHey interface {
    Hey() string // Hey 1回目
}

type Human struct{}
type Dog struct{}

func (Human) Hey() string {
    return "Hey!"
}

func (Dog) Hey() string {
    return "Bow Wow"
}

func main() {
    var hey SayHey
    hey = Human{}
    fmt.Println(hey.Hey())
    hey = Dog{}
    fmt.Println(hey.Hey())
}

1回だけで済む。やっぱりGoはシンプルで良いよね
ぜひとも、こんな感じに定義は1回だけにしたいものである。

作ったもの

インターフェース定義を元に上のようなコードを機械的に作ってくれるツール。

generated/hey.iface
# #から始まるのはコメント
package hey # これは名前空間になる

#そのまま#includeに置換される
import <string>
import <utility>

interface SayHey {
    hey() std::string = panic 
    # Go言語のように関数名、引数、戻り値の順番
    # `=`から後はinterfaceの中身がnullptrだったときの動作で
    # panicはthrow std::bad_function_call() に変換される
  # 何も指定しないと上の場合return std::string{}になる
}

生成コマンド
注: windows command promptです
D:\MiniTools\Utils> tool\ifacegen -h
Usage:
    tool\ifacegen [option]
Option:
    -o filename, --output-file filename : set output file
    -i filename, --input-file filename  : set input file
    -v, --verbose                       : verbose log
    -h, --help                          : show help
    (中略)
D:\MiniTools\Utils> tool\ifacegen -i generated/hey.iface -o generated/hey.cpp -v
PACKAGE:LITERAL_KEYWORD:package
PACKAGE:ID:hey
IMPORT:LITERAL_KEYWORD:import
IMPORT:UNTILEOL:<string>
IMPORT:LITERAL_KEYWORD:import
IMPORT:UNTILEOL:<utility>
INTERFACE:LITERAL_KEYWORD:interface
INTERFACE:ID:SayHey
INTERFACE:LITERAL_SYMBOL:{
FUNCDEF:ID:hey
FUNCDEF:LITERAL_SYMBOL:(
FUNCDEF:LITERAL_SYMBOL:)
TYPE:ID:std::string
FUNCDEF:LITERAL_SYMBOL:=
FUNCDEF:ID:panic
FUNCDEF:EOS:
INTERFACE:LITERAL_SYMBOL:}
INTERFACE:EOS:
ROOT:EOF:
generated code:
// Code generated by ifacegen (https://github.com/on-keyday/utils)

#pragma once
#include<helper/deref.h>
#include<functional>
#include<string>
#include<utility>

namespace hey {
struct SayHey {
   private:
    struct interface {
        virtual std::string hey() = 0;

        virtual ~interface(){}
    };

    template<class T>
    struct implements : interface {
        T t_holder_;

        template<class... Args>
        implements(Args&&...args)
            :t_holder_(std::forward<Args>(args)...){}

        std::string hey() override {
            auto t_ptr_ = utils::helper::deref(this->t_holder_);
            if (!t_ptr_) {
                throw std::bad_function_call();
            }
            return t_ptr_->hey();
        }

    };

    interface* iface = nullptr;

   public:
    constexpr SayHey(){}

    constexpr SayHey(std::nullptr_t){}

    template <class T>
    SayHey(T&& t) {
        iface=new implements<std::decay_t<T>>(std::forward<T>(t));
    }

    SayHey(SayHey&& in) {
        iface=in.iface;
        in.iface=nullptr;
    }

    SayHey& operator=(SayHey&& in) {
        delete iface;
        iface=in.iface;
        in.iface=nullptr;
        return *this;
    }

    explicit operator bool() const {
        return iface != nullptr;
    }

    ~SayHey() {
        delete iface;
    }

    std::string hey() {
        return iface?iface->hey():throw std::bad_function_call();
    }

};

} // namespace hey
generated
generated/hey.cpp(clang-format済み)
// Code generated by ifacegen (https://github.com/on-keyday/utils)

#pragma once
#include <helper/deref.h> // 自作ライブラリの一部
                           // Go言語と同じく
                           // ポインタをinterfaceに突っ込むための仕組みの補助
#include <functional>
#include <string>
#include <utility>

namespace hey {
    struct SayHey {
       private:
        struct interface {
            virtual std::string hey() = 0;

            virtual ~interface() {}
        };

        template <class T>
        struct implements : interface {
            T t_holder_;

            template <class... Args>
            implements(Args&&... args)
                : t_holder_(std::forward<Args>(args)...) {}

            std::string hey() override {
                auto t_ptr_ = utils::helper::deref(this->t_holder_);
                if (!t_ptr_) {
                    throw std::bad_function_call();
                }
                return t_ptr_->hey();
            }
        };

        interface* iface = nullptr;

       public:
        constexpr SayHey() {}

        constexpr SayHey(std::nullptr_t) {}

        template <class T>
        SayHey(T&& t) {
            iface = new implements<std::decay_t<T>>(std::forward<T>(t));
        }

        SayHey(SayHey&& in) {
            iface = in.iface;
            in.iface = nullptr;
        }

        SayHey& operator=(SayHey&& in) {
            delete iface;
            iface = in.iface;
            in.iface = nullptr;
            return *this;
        }

        explicit operator bool() const {
            return iface != nullptr;
        }

        ~SayHey() {
            delete iface;
        }

        std::string hey() {
            return iface ? iface->hey() : throw std::bad_function_call();
        }
    };

}  // namespace hey

やったね!

終わりに

Boost.TypeErasureとか似たようなの(微妙に趣旨が違う?)があるから
そこら辺使えば同じような事ができると思います。

参考リンク: https://developer.aiming-inc.com/programming/cpp-type-erasure/

現段階では演算子オーバーロード(operator()()とか)は面倒lexerが貧弱なので対応してません。必要になったら作ります。
[12/27追記]
関数名を__call__にすることで、operator()()を生成するようにしました。
ついでに、__copy__decltypeという関数名で、
コピーコンストラクタとコピー代入、元の型を取り出す(std::any_cast的なやつ)
が使えるようになります。

作ったツールのソースコードはここにあります。
(windowsでcmakeとclangとninjaの環境でビルド. linuxでbuildできるかは不明)
よかったら、使ってみてください。
一応、標準ライブラリ以外全部自作なのでバグがあったら、issueかなんかを立ててくだされば対応...するかもしれません。

何か間違っていることがあったら、遠慮無くコメントください。
似たようなツール見かけたら教えてください。

めんどくさいなって思ったら、Go言語使ってください。

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0