C++クイズ: Workarounds 回答と解説

公開:2020/12/19
更新:2020/12/26
21 min読了の目安(約19500字TECH技術記事

C++クイズ: Workarounds 回答と解説

この記事はC++ Advent Calendar 2020の19日目の記事です.

Iです.気づけばもう今年2週間切ってますね…


9日目の記事の想定回答と解説です.
未読の方は先に9日目の記事を読んで解いてみると楽しめるかと思います.


.
.
.







.
.
.


回答と解説

1. a : 関数名の変更

メンバ関数名が変わったのでいずれであっても呼べるラッパー関数を書きましょう,という問題です.
今回の問題においては awesome_typeaa2 の2つのメンバ関数を同時に持つことは無いため, a を持っている型では a を, a2 を持っている型では a2 を呼ぶようなラッパー関数を実装すれば良いでしょう.
これはC++17であればSFINAEを用いて実装することができます. enable_if を用いたSFINAEは以下のように記述します.

C++17 回答1
template<typename T, std::enable_if_t<
  std::is_same<decltype(std::declval<T>().a(std::declval<std::size_t>())),
               int
	      >::value, std::nullptr_t> = nullptr>
static constexpr int a(const T& t, std::size_t index){
  return t.a(index);
}

template<typename T, std::enable_if_t<
  std::is_same<decltype(std::declval<T>().a2(std::declval<std::size_t>())), 
               int
	      >::value, std::nullptr_t> = nullptr>
static constexpr int a(const T& t, std::size_t index){
  return t.a2(index);
}

「呼べるか呼べないか」のみを調べたい場合は,SFINAEの際戻り値型のチェックは必ずしも必要ありません(ただし,以下のような記述の場合は awesome_type::a または awesome_type::a2 の戻り値型が int ではない int に暗黙変換可能な型になった際エラー無く int にキャストして返すようになってしまいます).

C++17 回答2
namespace detail{

template<typename...>
struct detector_impl{
  using type = std::nullptr_t;
};
template<typename... Ts>
using detector = typename detector_impl<Ts...>::type;

}

template<typename T, detail::detector<
  decltype(std::declval<T>().a(std::declval<std::size_t>()))> = nullptr>
static constexpr int a(const T& t, std::size_t index){
  return t.a(index);
}

template<typename T, detail::detector<
  decltype(std::declval<T>().a2(std::declval<std::size_t>()))> = nullptr>
static constexpr int a(const T& t, std::size_t index){
  return t.a2(index);
}

また,今回のように移譲してそのまま結果を返す場合は以下のように戻り値型部分でSFINAEすることもできます.SFINAEとしてはこれが一番シンプル.

C++17 回答3
template<typename T>
static constexpr auto a(const T& t, std::size_t index) -> decltype(t.a(index)){
  return t.a(index);
}

template<typename T>
static constexpr auto a(const T& t, std::size_t index) -> decltype(t.a2(index)){
  return t.a2(index);
}

他に,constexpr ifを用いて分岐する方法もあります.
この場合はSFINAE-friendlyな has_a メタ関数が必要です(has_a メタ関数と std::enable_if を用いて回答2相当の記述を行うことも可能です).

C++17 回答4
namespace detail{

template<typename T, typename = std::nullptr_t>
struct has_a : std::false_type{};
template<typename T>
struct has_a<T, detector<decltype(
  std::declval<T>().a(std::declval<std::size_t>())
)>> : std::true_type{};

}

template<typename T>
static constexpr int a(const T& t, std::size_t index){
  if constexpr(detail::has_a<T>::value)
    return t.a(index);
  else
    return t.a2(index);
}

また,C++20の場合はコンセプトを用いて以下のように記述できます.

C++20 回答1
template<typename T>
requires(requires(const T& t){
  {t.a(std::declval<std::size_t>())} -> std::same_as<int>;
})
static constexpr int a(const T& t, std::size_t index){
  return t.a(index);
}

template<typename T>
requires(requires(const T& t){
  {t.a2(std::declval<std::size_t>())} -> std::same_as<int>;
})
static constexpr int a(const T& t, std::size_t index){
  return t.a2(index);
}
C++20 回答2
template<typename T>
requires(requires(const T& t){{t.a(std::declval<std::size_t>())};})
static constexpr int a(const T& t, std::size_t index){
  return t.a(index);
}

template<typename T>
requires(requires(const T& t){{t.a2(std::declval<std::size_t>())};})
static constexpr int a(const T& t, std::size_t index){
  return t.a2(index);
}
C++20 回答4
template<typename T>
static constexpr int a(const T& t, std::size_t index){
  if constexpr(requires(const T& t){{t.a(std::declval<std::size_t>())};})
    return t.a(index);
  else
    return t.a2(index);
}

コンセプト,記述が楽…

2. b : 戻り値型の変更

次は戻り値型が変わったので古いインターフェースに合わせましょう,という問題です.
さて, sal_wrapper::b でやりたいことは以下です.

  • awesome_type::b の戻り値型がポインタのとき: そのまま返す
  • awesome_type::b の戻り値型がoptionalのとき: 呼び出し結果の value_or(nullptr) を返す

つまり, awesome_type::b の戻り値型が const int*std::optional<const int*> かでSFINAEしてやれば良さそうです.

C++17 回答
template<typename T, std::enable_if_t<
  std::is_same<decltype(std::declval<T>().b(std::declval<std::size_t>())),
               const int*
	      >::value, std::nullptr_t> = nullptr>
static constexpr const int* b(const T& t, std::size_t index){
  return t.b(index);
}

template<typename T, std::enable_if_t<
  std::is_same<decltype(std::declval<T>().b(std::declval<std::size_t>())),
               std::optional<const int*>
	      >::value, std::nullptr_t> = nullptr>
static constexpr const int* b(const T& t, std::size_t index){
  return t.b(index).value_or(nullptr);
}

あるいは戻り値型が value_or メンバ関数を持っているか,などを確認することも可能です(が,直感的なコードでもないですし,避けたほうが懸命な気がします).
別解として,SFINAE-friendlyなメタ関数を用いたconstexpr if文による分岐も手です.

C++20ではコンセプトを用いてもう少し簡潔に記述できます.

C++20 回答
template<typename T>
requires(requires(const T& t){
  {t.b(std::declval<std::size_t>())} -> std::same_as<const int*>;
})
static constexpr const int* b(const T& t, std::size_t index){
  return t.b(index);
}

template<typename T>
requires(requires(const T& t){
  {t.b(std::declval<std::size_t>())} -> std::same_as<std::optional<const int*>>;
})
static constexpr const int* b(const T& t, std::size_t index){
  return t.b(index).value_or(nullptr);
}

3. c : 関数定義位置の変更

関数の定義位置が変わったので適切な関数に移譲する c を実装したい,という問題.
さて,この問題には厄介な点が2つあります.
1つは,上記2問で扱ったようにある型 T が(静的/非静的)メンバ関数を持つかどうかを検査するメタ関数は実装が可能ですが,非メンバ関数の存在有無はSFINAE-friendlyな形で実装できないということです(実装できないと私は思ってるんですがもしかして方法あったりするんですかね?).
尤も,この点については(少なくとも今回の問題では awesome_type::c が無ければ非メンバ関数の c が存在するため) awesome_typec を持つか否かのメタ関数を用いることで対処可能です.
もう1つは,非メンバ関数のシンボル名は普通に記述すると依存名にならないため,以下のような記述をすると SUPER_AWESOME_LIBRARY_THAT_HAS_BREAKING_CHANGES_FREQUENTLY_VERSION2 の時 c が名前空間 super_(ry に含まれない,というエラーが出てしまいます.

template<typename... Args,
         typename T = super_awesome_library_that_has_breaking_changes_frequently::awesome_type,
	 std::enable_if_t<いい感じのhas_c<T>::value, std::nullptr_t> = nullptr>
static constexpr int c(Args&&... args){
  return T::c(std::forward<Args>(args)...);
}

template<typename... Args,
         typename T = super_awesome_library_that_has_breaking_changes_frequently::awesome_type,
	 std::enable_if_t<!いい感じのhas_c<T>::value, std::nullptr_t> = nullptr>
static constexpr int c(Args&&... args){
  return super_awesome_library_that_has_breaking_changes_frequently::c(
           std::forward<Args>(args)...
	 );  //ここでエラー
}

これはなぜかと言うと, super_(ry ::c はtemplate引数の依存名ではないため,Two phase lookupの1周目(インスタンス化前)でlookupが走ってしまうためです.
バージョン2において下の関数はインスタンス化されないことを想定していますが,その如何に関わらず名前空間 super_(ry の下に c が存在するかどうかの確認は行われてしまうわけです.

さて,この問題の解法はざっくり以下の2通りとなります.

解法1. 上記のようにメタ関数で呼び分ける.この際,非メンバ関数の c をなんとかして依存名にする
解法2. super_(ry 名前空間にバージョン2用の c を書いてしまう

準備(C++17)

さて,C++17の場合いずれの解法においてもある型 T が適切なシグネチャの非メンバ関数 c を持つかをSFINAE-friendlyに bool 値で得る手段が必要ですので,先にそちらを実装します.
デフォルト引数と可変長引数の相性が悪いので,ここでは優先度を持たせた関数オーバーロードによるSFINAEを行います.

C++17 has_c
namespace detail{

template<std::size_t N>struct priority : priority<N-1>{};
template<>struct priority<0>{};

struct has_c_impl{
  template<typename T, typename... Args>
  static auto check(priority<1>, Args&&...)
    -> decltype(T::c(std::declval<Args>()...), std::true_type{});
  template<typename T, typename... Args>
  static auto check(priority<0>, Args&&...) -> std::false_type;
};

template<typename T, typename... Args>
using has_c = decltype(has_c_impl::check<T>(priority<1>{}, std::declval<Args>()...));

}

priority<1> 型は priority<0> 型の派生型なので, priority<1> を引数に持つ関数の方がオーバーロード解決時に優先されます.
これによりオーバーロード解決の曖昧さを解消できます.

なお,C++20の場合はコンセプトでねじ伏せていけるので上記メタ関数は不要です.

解法1. c を依存名にする

さて,上述の通り普通に関数呼び出しを行うと依存名にはならないわけですが,非メンバ関数呼び出しを依存名にする方法,実はあります.
関数呼び出し解決をADLに委ねる のです.

C++17 解法1
namespace detail{

template<typename... Args,
         typename T = super_awesome_library_that_has_breaking_changes_frequently::awesome_type,
	 std::enable_if_t<detail::has_c<T, Args...>::value, std::nullptr_t> = nullptr>
static constexpr int c_impl(Args&&... args){
  return T::c(std::forward<Args>(args)...);
}

template<typename... Args,
         typename T = super_awesome_library_that_has_breaking_changes_frequently::awesome_type,
	 std::enable_if_t<!detail::has_c<T, Args...>::value, std::nullptr_t> = nullptr>
static constexpr int c_impl(Args&&... args){
  using namespace super_awesome_library_that_has_breaking_changes_frequently; //cが存在するかは不明なので名前空間全体を導入する
  return c(std::forward<Args>(args)...); //ADL
}

}

template<typename... Args>
static constexpr int c(Args&&... args){
  return detail::c_impl(std::forward<Args>(args)...);
}

引数 std::forward<Args>(args) が依存名なので,ADLによる関数ルックアップを行えば呼び出し関数は依存名となります.やったぜ.
ただしADLで super_(ry ::cが呼び出される保証は必ずしもないので,この解法は関数名が比較的uniqueなときに限ったほうが良いでしょう.
また,実装関数の名前を c にしてしまうと自己再帰してしまうので必ず c でない名前(上記では c_impl)にします.

C++20の場合は以下のようになります.

C++20 解法1
namespace detail{

template<typename... Args,
         typename T = super_awesome_library_that_has_breaking_changes_frequently::awesome_type
>
requires(requires{{T::c(std::declval<Args>()...)};})
static constexpr int c_impl(Args&&... args){
  return T::c(std::forward<Args>(args)...);
}

template<typename... Args,
         typename T = super_awesome_library_that_has_breaking_changes_frequently::awesome_type
>
requires(!requires{{T::c(std::declval<Args>()...)};})
static constexpr int c_impl(Args&&... args){
  using namespace super_awesome_library_that_has_breaking_changes_frequently;
  return c(std::forward<Args>(args)...);
}

}

template<typename... Args>
static constexpr int c(Args&&... args){
  return detail::c_impl(std::forward<Args>(args)...);
}

解法2. super_(ry 名前空間上に直接オーバーロードを生やす

もう1つの解法は,バージョン2用の csuper_(ry 名前空間上に直接記述してしまうというものです.
やや行儀の悪い解法ですが, 制約 の節で

super_awesome_library_that_has_breaking_changes_frequently 名前空間内は汚染して良いものとする.

としたので問題ありません(というか,この解法のためにこの条項を制約に追加しました).

バージョン2用の cawesome_type に静的メンバ関数 c がある場合はそれに処理を移譲するような関数ですが,これを super_(ry に直接記述すると

  • バージョン1: awesome_type は静的メンバ関数 c を持たないので本来の非メンバ関数 c が呼び出される
  • バージョン2: super_(ry 名前空間上の関数 csal_wrapper 側で実装したもののみなので,これが呼ばれる.この際, awesome_type は静的メンバ関数 c を持つので正しく処理を移譲する.

となり, super_(ry ::c がバージョン1でもバージョン2でも期待したように振る舞うようになります.
問題文では sal_wrapper::c を要求しているので,最後に super_(ry ::csal_wrapper 上に using してやれば題意を満たします.

C++17 解法2
namespace super_awesome_library_that_has_breaking_changes_frequently{

template<typename... Args,typename T = awesome_type,
  std::enable_if_t<
    sal_wrapper::detail::has_c<T, Args...>::value,
  std::nullptr_t> = nullptr>
static constexpr int c(Args&&... args){
  return T::c(std::forward<Args>(args)...);
}

}

namespace sal_wrapper{

using super_awesome_library_that_has_breaking_changes_frequently::c;

}

C++20では以下となります.

C++20 解法2
namespace super_awesome_library_that_has_breaking_changes_frequently{

template<typename... Args, typename T = awesome_type>
requires(requires(const T& t){{T::c(std::declval<Args>()...)};})
static constexpr int c(Args&&... args){
  return T::c(std::forward<Args>(args)...);
}

}

namespace sal_wrapper{

using super_awesome_library_that_has_breaking_changes_frequently::c;

}

4. d : 仮想関数の引数の変更

仮想関数の引数が増えたので,それを隠蔽できるような親クラスを作る問題.
バージョン1の場合は sal_wrapper::excellent_basesuper_(ry ::excellent_type を指せば良く,
バージョン2の場合, void d(int, super_flags)void d(int) に移譲されるような excellent_base を実装し,
これらを super_(ry ::excellent_type::d のシグネチャに応じて切り替えるようにします.

C++17 回答
namespace detail{

template<typename T>
struct excellent_base : T{
  virtual void d(int) = 0;
  virtual void d(
      int value,
      super_awesome_library_that_has_breaking_changes_frequently::super_flags
  )override{
    this->d(value);
  }
};

template<typename T>
class d_with_super_flags{
  template<typename U>
  static std::true_type impl(
    decltype(std::declval<U>().d(
      std::declval<int>(),
      std::declval<super_awesome_library_that_has_breaking_changes_frequently::super_flags>()
    ))*
  );
  template<typename U>
  static std::false_type impl(
    decltype(std::declval<U>().d(
      std::declval<int>()
    ))*
  );
 public:
  static constexpr bool value = decltype(impl<T>(nullptr))::value;
};

template<typename T>
using excellent_base_switcher_t = std::conditional_t<
  d_with_super_flags<T>::value,
  excellent_base<T>,
  T
>;

}

using excellent_base = detail::excellent_base_switcher_t<
  super_awesome_library_that_has_breaking_changes_frequently::excellent_type
>;

excellent_baseexcellent_type の派生型である必要があるので,バージョン2の excellent_type を念頭に2引数の d を持つと仮定して記述し,1引数の d はユーザーに向けて純粋仮想関数にしておきます.
直接 excellent_type を継承するのではなく,template引数の型を継承することでインスタンス化まで実際のメンバ関数のシグニチャのチェックを遅延させます.
あとはメンバ関数 d のシグネチャがバージョン2のものかどうかをチェックするメタ関数を使って std::conditional で型をスイッチします.

C++20では以下のようになります.

C++20 回答
namespace detail{

template<typename T>
struct excellent_base : T{
  virtual void d(int) = 0;
  virtual void d(
      int value,
      super_awesome_library_that_has_breaking_changes_frequently::super_flags
  )override{
    this->d(value);
  }
};

template<typename T>
using excellent_base_switcher = std::conditional_t<
  requires(T& t){
    {t.d(std::declval<int>(),
         std::declval<super_awesome_library_that_has_breaking_changes_frequently::super_flags>())
    } -> std::same_as<void>;
  },
  excellent_base<T>, T
>;

}

using excellent_base = detail::excellent_base_switcher<
  super_awesome_library_that_has_breaking_changes_frequently::excellent_type
>;

ただし,g++ 10ではusing aliasと std::same_as の併用がエラーになるバグがあるため, excellent_base_switcher について以下のいずれかのようなworkaroundが必要(g++ 11では修正済みの模様).

workaround1 std
namespace detail{

template<typename T>
struct excellent_base_switcher : std::conditional<
  requires(T& t){
    {t.d(std::declval<int>(),
         std::declval<super_awesome_library_that_has_breaking_changes_frequently::super_flags>())
    } -> std::same_as<void>;
  },
  excellent_base<T>, T
>{};

}

using excellent_base = detail::excellent_base_switcher<
  super_awesome_library_that_has_breaking_changes_frequently::excellent_type
>::type; //ここでtypeをとる
workaround2 same_asを使わずに戻り値型をチェックする
template<typename T>
using excellent_base_switcher = std::conditional_t<
  requires(T& t){
    {t.d(std::declval<int>(),
         std::declval<super_awesome_library_that_has_breaking_changes_frequently::super_flags>())
    };
    std::is_void<
      decltype(t.d(
        std::declval<int>(),
	std::declval<super_awesome_library_that_has_breaking_changes_frequently::super_flags>()
      ))
    >::value;
  },
  excellent_base<T>, T
>;

5. hypers : range対応

いい感じのrangeを作る問題.
いい感じのrangeを作り, excellent_type::access_hyper_dataexcellent_type::hypers といったメンバ関数の有無に応じていい感じのrangeを返したり excellent_type::hypers をそのまま返したりすればOKです.

C++17 回答
namespace detail{

template<typename T>
struct hyper_range{
  T* t;
 public:
  class iterator{
    class accessor{
      T* t;
      std::size_t index;
     public:
      accessor() = default;
      accessor(T* ptr, std::size_t ind) : t{ptr}, index{ind}{}
      accessor(const accessor&) = default;
      accessor(accessor&&) = default;
      accessor& operator=(const accessor&) = default;
      accessor& operator=(accessor&) = default;
      const std::string& access()const{
        return t->access_hyper_data(index);
      }
      friend class iterator;
    }a;
   public:
    using difference_type = std::ptrdiff_t;
    using value_type = accessor;
    using iterator_category = std::bidirectional_iterator_tag;
    iterator() = default;
    iterator(T* ptr, std::size_t ind) : a{ptr, ind}{}
    iterator(const iterator&) = default;
    iterator(iterator&&) = default;
    iterator& operator=(const iterator&) = default;
    iterator& operator=(iterator&&) = default;
    iterator& operator++(){
      ++a.index;
      return *this;
    }
    iterator operator++(int){
      iterator it = *this;
      ++*this;
      return it;
    }
    iterator& operator--(){
      --a.index;
      return *this;
    }
    iterator operator--(int){
      iterator it = *this;
      --*this;
      return it;
    }
    bool operator==(const iterator& rhs)const{
      return a.index == rhs.a.index;
    }
    bool operator!=(const iterator& rhs)const{
      return !(*this == rhs);
    }
    const accessor& operator*()const{return a;}
    const accessor* operator->()const{return &a;}
  };
  constexpr explicit hyper_range(T* ptr):t{ptr}{}
  iterator begin()const{return iterator{t, 0u};}
  iterator end()const{return iterator{t, t->hyper_size()};}
};

}

template<typename T, std::enable_if_t<
  std::is_same<
    decltype(std::declval<T>().access_hyper_data(std::declval<std::size_t>())),
    const std::string&
  >::value, std::nullptr_t> = nullptr>
static detail::hyper_range<const T> hypers(const T& t){
  return detail::hyper_range<const T>{&t};
}

template<typename T, std::enable_if_t<
  std::is_same<
    decltype(std::declval<T>().hypers().begin()->access()),
    const std::string&
  >::value,std::nullptr_t> = nullptr>
static auto hypers(const T& t){
  return t.hypers();
}
C++20 回答
namespace detail{

template<typename T>
struct hyper_range{
  T* t;
 public:
  class iterator{
    class accessor{
      T* t;
      std::size_t index;
     public:
      accessor() = default;
      accessor(T* ptr, std::size_t ind) : t{ptr}, index{ind}{}
      accessor(const accessor&) = default;
      accessor(accessor&&) = default;
      accessor& operator=(const accessor&) = default;
      accessor& operator=(accessor&&) = default;
      const std::string& access()const{
        return t->access_hyper_data(index);
      }
      friend class iterator;
    }a;
   public:
    using difference_type = std::ptrdiff_t;
    using value_type = accessor;
    using iterator_concept = std::bidirectional_iterator_tag;
    iterator() = default;
    iterator(T* ptr, std::size_t ind) : a{ptr, ind}{}
    iterator(const iterator&) = default;
    iterator(iterator&&) = default;
    iterator& operator=(const iterator&) = default;
    iterator& operator=(iterator&&) = default;
    iterator& operator++(){
      ++a.index;
      return *this;
    }
    iterator operator++(int){
      iterator it = *this;
      ++*this;
      return it;
    }
    iterator& operator--(){
      --a.index;
      return *this;
    }
    iterator operator--(int){
      iterator it = *this;
      --*this;
      return it;
    }
    bool operator==(const iterator& rhs)const{
      return a.index == rhs.a.index;
    }
    bool operator!=(const iterator& rhs)const{
      return !(*this == rhs);
    }
    const accessor& operator*()const{return a;}
    const accessor* operator->()const{return &a;}
  };
  constexpr explicit hyper_range(T* ptr):t{ptr}{}
  iterator begin()const{return iterator{t, 0u};}
  iterator end()const{return iterator{t, t->hyper_size()};}
};

}

template<typename T>
requires(requires(const T&){
  {std::declval<T>().access_hyper_data(std::declval<std::size_t>())}
    -> std::same_as<const std::string&>;
})
static detail::hyper_range<const T> hypers(const T& t){
  return detail::hyper_range<const T>{&t};
}

template<typename T>
requires(requires(const T&){
  {std::declval<T>().hypers().begin()->access()}
    -> std::same_as<const std::string&>;
})
static auto hypers(const T& t){
  return t.hypers();
}

}

最後の方疲れて雑になったな?まぁ5問目は語ることも特にないしええか…

明日も私です.よろしくお願いします.まだ何も書いてませんが…