Help us understand the problem. What is going on with this article?

C/C++で関数ポインタを持つ配列からenum定数を用いて関数呼び出しする

マサカリ募集中です。
どうぞお気軽に投擲していってください。

※ 2019/3/11 一部更新しました。

こんなの

test.cpp
#include <iostream>
#include <string>
#include <functional>
#include <array>


enum class event{
    event1,
    event2,
    end
};

void func1(std::string && str){std::cout << "[func1:]" << str << std::endl;}
void func2(std::string && str){std::cout << "[func2:]" << str << std::endl;}


void (*funclist[static_cast<int>(event::end)])(std::string && str) = {
                                                                   func1, //event1の値を元に呼び出したい関数
                                                                   func2 //event2の値を元に呼び出したい関数
                                                                   };

int main(){
    funclist[static_cast<int>(event::event1)]("event");
    funclist[static_cast<int>(event::event2)]("event");
}

これを格納順番に依存しないようにしつつ、かつエレガントに記述したい!

どうするか

パッと思いつくのは以下の方法

  1. enum定数と関数ポインタを同時に保持して呼び出しの都度探索
  2. 実行時に配列へ関数ポインタを挿入する関数を作成し実行時に毎回呼び出し
  3. 諦めて拡張の都度順番を確認しながら挿入していく

1,2はどうしてもコストがかかってしまうのであまり用いたくありません。
と言う訳で諦めて3に行ってしまう前に、C/C++でどのようにすれば解決できるかを考えていきたいと思います。

補足(12/2追加)
コメントで教えていただきましたが、設計図を元にツールでコードの自動生成を行うと言った方法を取るという選択肢もあるようです。
より良いシステム開発のために、状態遷移設計のことを知ってほしい - Qiita
ツールで自動化出来るようなシチュエーションであれば作って運用するのが一番安全でてっとり早いかもしれません。

Cの場合

C99なら指示付き初期化を使うことによって解決します。

test.c
#include <stdio.h>

enum event{
    event1,
    event2,
    event3,
    event4,
    end
};

void func1(const char * str){
    printf("[func1]%s\n",str);
}
void func3(const char * str){
    printf("[func3]%s\n",str);
}

void (*eventlist[end+1])(const char *) = {
                         [event1] = func1, //event1の値を元に呼び出したい関数
                         [event3] = func3, //event3の値を元に呼び出したい関数
                         [end] = NULL,
                         };

//歯抜け対策用に一枚かませる
inline void callEvent(enum event ev,const char *str);

int main(void){
    callEvent(event1,"event");
    callEvent(event2,"event");
    callEvent(event3,"event");
    callEvent(event4,"event");

    return 0;
}

void callEvent(enum event ev,const char *str){
    if(ev >= 0 && ev < end && eventlist[ev]){ //範囲外or関数ポインタ未挿入であれば実行しない
        eventlist[ev](str);
    }
}

ですがC++では配列の指示付き初期化は定義されておらず、規格の上では使用することができません。1

C++の場合

constexprコンストラクタを利用して初期化します

test.cpp
#include <iostream>
#include <string>

#include <cstdint>

enum class event : uint32_t{
    event1,
    event2,
    event3,
    event4,
    end
};

void func1(std::string && str){std::cout << "[func1]" << str << std::endl;}
void func3(std::string && str){std::cout << "[func3]" << str << std::endl;}

struct func{
    constexpr func() : eventlist(){
        eventlist[static_cast<uint32_t>(event::event1)] = func1; //event1の値を元に呼び出したい関数
        eventlist[static_cast<uint32_t>(event::event3)] = func3; //event3の値を元に呼び出したい関数
    }

    //歯抜け対策用に一枚かませる
    void callEvent(event ev,std::string && str) const{

        auto val = static_cast<uint32_t>(ev);

        if(val < static_cast<uint32_t>(event::end) && eventlist[val]){ //範囲外or関数ポインタ未挿入であれば実行しない
            eventlist[val](std::move(str));
        }
    }
private:
    void (*eventlist[static_cast<uint32_t>(event::end)])(std::string && str);
};

constexpr func f;

int main(){
    f.callEvent(event::event1,"event");
    f.callEvent(event::event2,"event");
    f.callEvent(event::event3,"event");
    f.callEvent(event::event4,"event");
}

以上のコードはC++14以降で動きます。

ラッパークラスを作ったり関数の呼び出し毎にオブジェクト名を記述する手間が増えますが、
上で挙げた3つの方法を採用するのに比べれば安いものでしょう

またC++17以降であれば関数の代入時にラムダ式を利用することもできます。

test.cpp
#include <iostream>
#include <string>

#include <cstdint>

enum class event : uint32_t{
    event1,
    event2,
    event3,
    event4,
    end
};

struct func{
    constexpr func() : eventlist(){
        eventlist[static_cast<uint32_t>(event::event1)] = [](std::string && str){std::cout << "[func1:]" << str << std::endl;}; //event1の値を元に呼び出したい関数
        eventlist[static_cast<uint32_t>(event::event3)] = [](std::string && str){std::cout << "[func3:]" << str << std::endl;};  //event3の値を元に呼び出したい関数
    }

    //歯抜け対策用に一枚かませる
    void callEvent(event ev,std::string && str) const{

        auto val = static_cast<uint32_t>(ev);

        if(val < static_cast<uint32_t>(event::end) && eventlist[val]){ //範囲外or関数ポインタ未挿入であれば実行しない
            eventlist[val](std::move(str));
        }
    }
private:
    void (*eventlist[static_cast<uint32_t>(event::end)])(std::string && str);
};

constexpr func f;

int main(){
    f.callEvent(event::event1,"event");
    f.callEvent(event::event2,"event");
    f.callEvent(event::event3,"event");
    f.callEvent(event::event4,"event");
}

まとめ

と言う訳でパッと思いついた方法を取らずになんとか順番に依存しない初期化を行うことができました。

と言っても個人的にはなんかモヤモヤする点もあるのでもっといい方法があれば是非教えてください。

参考資料


  1. 規格では定義されていないようですが、一部の主要コンパイラではC++でも記述できるようです。 

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away