LoginSignup
0
1

More than 1 year has passed since last update.

呼び出し可能な場合だけ呼び出しを行う

Last updated at Posted at 2022-04-28

はじめに

ラッパーの宿命としてライブラリの更新でビルドが通らなくなる可能性は常にあると思います。その中で、ビルド可能な関数だけビルドし他の関数は諦めるという機能の需要も少なからずあると思います。 動的言語のbindingの場合は特に。
通常「ビルド可能」かどうかの判定にはconfigure(やCMakeのcheck_cxx_source_compiles)を使うのだと思いますが、パターンが膨大になると厳しいものがあります1
今回はSFINAEで達成できないか考えてみました。

方法

準備

  • 本記事で扱う手法はダミーのtemplateを使います。何でも良いのでtypenameで関数を宣言します。変数名がmなのはもとはPYBIND11_MODULEの第2引数を持ってきたからです。
    • Ruby Riceの場合はRice::define_moduleの返し値を渡せば良いと思います。
  • constexpr(std::is_invocable)で囲います(is_invocableの使い方は既習とします)。
#include <type_traits>
#include <functional>

#include <cstdio>

struct A{
    int fi(int n){printf("CALL %d\n", n);return n;}
};

template<typename M>
void impl(M m){
    if constexpr(std::is_invocable<decltype(&A::fi), A&, int>::value){
        A a;
        a.fi(0);
    }
}

int main(){
    void *p = NULL;
    impl(p);
}

関数を隠す

ここで、ライブラリに仕様変更があり引数の数が変わったとします。ここでimplには関数の実行を黙って諦めてもらいたいのですが、no matching function for callとなってしまいます。これはSFINAEの影響でコンパイルされないのはtwo-phase name lookupの場合に限られるからです。なのでa.fi(0)呼び出しをテンプレートに突っ込む必要があります。

  • a.fi(0)std::invoke(&A::fi, a, 0)と変形します。
  • &A::fiTwoPhaseValue<M, &A::fi>::valueと変形します。

以下のプログラムは fiの型に関わらずコンパイルが通ります。

#include <type_traits>
#include <functional>

// lib

// delay value_ to be instantiated by using two-phase name lookup (note: useless if not using SFINAE)
template<typename M, auto value_>
struct TwoPhaseValue
{
    constexpr static decltype(value_) value = value_;
};

// prog

#include <cstdio>

struct A{
    int fi(int n, int m){printf("CALL %d\n", n);return n;}
};

template<typename M>
void impl(M m){
    if constexpr(std::is_invocable<decltype(&A::fi), A&, int>::value){
        A a;
        std::invoke(TwoPhaseValue<M, &A::fi>::value, a, 0);
    }
}

int main(){
    void *p = NULL;
    impl(p);
}

変更に追従する

さて実際に達成したいことはライブラリの新旧バージョンいずれでも動作するプログラムを作ることでした。
まず(開発版では)コンパイルが失敗したことを検知できるようにしたいです。
そのためにはfalse_vを使います。
また、このif constexprはかなりの数出現するので、マクロで管理できるようにします。

あとはif constexprとTwoPhaseValueを並べていけばよいです。

#include <type_traits>
#include <functional>

// lib

template <typename T>
constexpr bool false_v = false;

// should be 0 by default (set to 1 for local compile testing)
#define CHECK_MISSING_DEF 0
#if CHECK_MISSING_DEF
#define STRINGIFY(n) #n
#define TOSTRING(n) STRINGIFY(n)
#define MISSING_DEF_CHECKER else{static_assert(false_v<M>, "bad T in line " TOSTRING(__LINE__));}
#else
#define MISSING_DEF_CHECKER
#endif

// delay value_ to be instantiated by using two-phase name lookup (note: useless if not using SFINAE)
template<typename M, auto value_>
struct TwoPhaseValue
{
    constexpr static decltype(value_) value = value_;
};

// prog

#include <cstdio>

struct A{
    int fi(int n, int m){printf("CALL %d\n", n);return n;}
};

template<typename M>
void impl(M m){
    if constexpr(std::is_invocable<decltype(&A::fi), A&, int>::value){
        A a;
        std::invoke(TwoPhaseValue<M, &A::fi>::value, a, 0);
    }
    else if constexpr(std::is_invocable<decltype(&A::fi), A&, int, int>::value){
        A a;
        std::invoke(TwoPhaseValue<M, &A::fi>::value, a, 0, 0);
    } MISSING_DEF_CHECKER
}

int main(){
    void *p = NULL;
    impl(p);
}

オーバーロード関数

さてis_invocableですが、残念ながらオーバーロード関数には使うことができません。関数のポインタが取れないため、です。
インスタンスを受け取る関数のマクロを定義し2、std::experimental::is_detected3と組み合わせることで解決します。実際に呼び出す関数もtwo-phase name lookup内に隠蔽する必要があるのは変わりません。

なお、この手法は、バージョンによっては 存在しない可能性のある関数 を呼び出すのにも使えます。

#define GENERATE_INVOCABLE_BY_MEMBER(member) \
template<typename T, typename ...Ts> using invocable_##member##_by = decltype(std::declval<T&>().member(std::declval<Ts>()...));

#define LIFT(inst, member) ([&inst](auto&&... xs) -> decltype(auto) { return inst.member(::std::forward<decltype(xs)>(xs)...); })
#define GENERATE_TWOPHASE_CALL_MEMBER(member) \
template<typename M, typename T, class ...Args> auto TwoPhase_##member(T &p, Args... args){return std::invoke(LIFT(p, member), args...);}
#include <type_traits>
#include <functional>

#include <experimental/type_traits>

// lib

template <typename T>
constexpr bool false_v = false;

// should be 0 by default (set to 1 for local compile testing)
#define CHECK_MISSING_DEF 0
#if CHECK_MISSING_DEF
#define STRINGIFY(n) #n
#define TOSTRING(n) STRINGIFY(n)
#define MISSING_DEF_CHECKER else{static_assert(false_v<M>, "bad T in line " TOSTRING(__LINE__));}
#else
#define MISSING_DEF_CHECKER
#endif

// delay value_ to be instantiated by using two-phase name lookup (note: useless if not using SFINAE)
template<typename M, auto value_>
struct TwoPhaseValue
{
    constexpr static decltype(value_) value = value_;
};

// https://stackoverflow.com/questions/45249985/how-to-require-an-exact-function-signature-in-the-detection-idiom/45250180#45250180
// note: only for overloaded or might-not-exist methods
#define GENERATE_INVOCABLE_BY_MEMBER(member) \
template<typename T, typename ...Ts> using invocable_##member##_by = decltype(std::declval<T&>().member(std::declval<Ts>()...));

// https://stackoverflow.com/questions/45505017/how-comes-stdinvoke-does-not-handle-function-overloads/45506294#45506294
// note: only for overloaded or might-not-exist methods
#define LIFT(inst, member) ([&inst](auto&&... xs) -> decltype(auto) { return inst.member(::std::forward<decltype(xs)>(xs)...); })
#define GENERATE_TWOPHASE_CALL_MEMBER(member) \
template<typename M, typename T, class ...Args> auto TwoPhase_##member(T &p, Args... args){return std::invoke(LIFT(p, member), args...);}

GENERATE_INVOCABLE_BY_MEMBER(fo)
GENERATE_TWOPHASE_CALL_MEMBER(fo)

// prog

#include <cstdio>

struct A{
    int fi(int n, int m=2){printf("CALL fi %d\n", n);return n;}
    int fo(double d){printf("CALL fo double\n");return 0;}
    int fo(void *p){printf("CALL fo void*\n");return 0;}
};

template<typename M>
void impl(M m){
    if constexpr(std::is_invocable<decltype(&A::fi), A&, int>::value){
        A a;
        std::invoke(TwoPhaseValue<M, &A::fi>::value, a, 0);
    }
    else if constexpr(std::is_invocable<decltype(&A::fi), A&, int, int>::value){
        A a;
        std::invoke(TwoPhaseValue<M, &A::fi>::value, a, 1, 0);
    } MISSING_DEF_CHECKER

#if 0
    // compile error
    if constexpr(std::is_invocable<decltype(&A::fo), A&, double>::value){
        A a;
        std::invoke(TwoPhaseValue<M, &A::fo>::value, a, 1.0);
    } MISSING_DEF_CHECKER
#endif

    if constexpr(std::experimental::is_detected<invocable_fo_by, A&, double>::value){
        A a;
        TwoPhase_fo<M>(a, 1.0);
    } MISSING_DEF_CHECKER
}

int main(){
    void *p = NULL;
    impl(p);
}

おわりに

C++って結構(単純なラッパーに限らず)polyfill的なこともできるのでないかという気がします(C++17以降限定ですが)。

  1. 記事を書く前に実際に作ったプログラムでは数百個

  2. マクロの数だけ汚くなるので、できればオーバーロード関数・存在しない可能性のある関数は少ないほうが良いですね。。

  3. experimentalがあれな場合はboost::is_detectedでも良いです(ヘッダは#include <boost/type_traits/is_detected.hpp>)

0
1
0

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
0
1