はじめに
ラッパーの宿命としてライブラリの更新でビルドが通らなくなる可能性は常にあると思います。その中で、ビルド可能な関数だけビルドし他の関数は諦めるという機能の需要も少なからずあると思います。 動的言語の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::fi
をTwoPhaseValue<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以降限定ですが)。