C++20 将抽象class(接口)和mixins 转换为概念的最佳方法

C++20 best way to convert abstract class (interface) and mixins into concept

我曾经通过摘要class来定义我的模板要求,例如

#include <iostream>
#include <random>

/// Generic interface
template<typename A, typename B>
struct Interface {
  virtual A callback_A(const std::vector<A>& va) = 0;
  virtual const B& callback_B() = 0;
};

/// Mixin style, used to "compose" using inheritance at one level, no virtual
struct PRNG_mt64 {
  std::mt19937_64 prng;
  explicit PRNG_mt64(size_t seed) : prng(seed) {};
};

/// Our implementation
template<typename A>
struct Implem :
  public Interface<A, std::string>,
  public PRNG_mt64 {

  std::string my_string{"world"};

  explicit Implem(size_t seed) : PRNG_mt64(seed) {}

  A callback_A(const std::vector<A>& a) override { return a.front(); }

  const std::string& callback_B() override { return my_string; }

};

/// Function using our type. Verification of the interface is perform "inside" the function
template<typename T>
void use_type(T& t) {
  auto& strings = static_cast<Interface<std::string, std::string>&>(t);
  std::cout << strings.callback_A({"hello"}) << " " << strings.callback_B() << std::endl;
  auto& prng = static_cast<PRNG_mt64&>(t).prng;
  std::uniform_real_distribution<double> dis(0.0, 1.0);
  std::cout << dis(prng) << std::endl;
}

int main(int argc, char **argv) {
  size_t seed = std::random_device()();
  Implem<std::string> my_impl(seed);
  use_type(my_impl);
}

使用 asbtract class 的一个好处是接口规范清晰,易于阅读。另外,Implem 必须符合它(我们不能忘记纯虚拟)。

一个问题是接口要求隐藏在静态转换中(来自我的真实用例,其中复合“状态”被多个多态组件使用 - 每个组件都可以转换状态以仅查看它的内容需要看)。这是通过概念“解决”的(见下文)。

另一个是我们在根本没有动态多态性的情况下使用虚拟机制,所以我想摆脱它们。将此“界面”转换为概念的最佳方式是什么? 我想到了这个:

#include <iostream>
#include <random>

/// Concept "Interface" instead of abstract class
template<typename I, typename A, typename B>
concept Interface = requires(I& impl){
  requires requires(const std::vector<A>& va){{ impl.callback_A(va) }->std::same_as<A>; };
  { impl.callback_B() } -> std::same_as<const B&>;
};

/// Mixin style, used to "compose" using inheritance at one level, no virtual
struct PRNG_mt64 {
  std::mt19937_64 prng;
  explicit PRNG_mt64(size_t seed) : prng(seed) {};
};

/// Our implementation
template<typename A>
struct Implem : public PRNG_mt64 {

  std::string my_string{"world"};

  /// HERE: requires in the constructor to "force" interface. Can we do better?
  explicit Implem(size_t seed) requires(Interface<Implem<A>, A, std::string>): PRNG_mt64(seed) {}

  A callback_A(const std::vector<A>& a) { return a.front(); }

  const std::string& callback_B() { return my_string; }
};

/// Function using our type. Verification of the interface is now "public"
template<Interface<std::string, std::string> T>
void use_type(T& t) {
  std::cout << t.callback_A({"hello"}) << " " << t.callback_B() << std::endl;
  auto& prng = static_cast<PRNG_mt64&>(t).prng;
  std::uniform_real_distribution<double> dis(0.0, 1.0);
  std::cout << dis(prng) << std::endl;
}

int main(int argc, char **argv) {
  size_t seed = std::random_device()();
  Implem<std::string> my_impl(seed);
  use_type(my_impl);
}

问题:

谢谢!

你的 pre-C++20 方法很糟糕,但至少听起来你理解它的问题。也就是说,当你不需要 vptr 时,你要为它支付 8 个字节;然后 strings.callback_B() 支付了虚拟呼叫的费用,即使您 可以 直接呼叫 t.callback_B()

最后(这是相关的,我保证),通过 base-class 引用 strings 汇集所有内容,你正在剥夺 Implem 制作有用重载的能力放。我将向您展示一个更简单的示例:

struct Interface {
  virtual int lengthOf(const std::string&) = 0;
};
struct Impl : Interface {
  int lengthOf(const std::string& s) override { return s.size(); }
  int lengthOf(const char *p) { return strlen(p); }
};

template<class T>
void example(T& t) {
  Interface& interface = t;
  static_assert(!std::same_as<decltype(interface), decltype(t)>);  // Interface& versus Impl&
  int x = interface.lengthOf("hello world");  // wastes time constructing a std::string
  int y = t.lengthOf("hello world");  // does not construct a std::string
}
int main() { Impl impl; example(impl); }

generic-programming 方法在 C++20 中看起来像这样:

template<class T>
concept Interface = requires (T& t, const std::string& s) {
  { t.lengthOf(s) } -> convertible_to<int>;
};
struct Impl {
  int lengthOf(const std::string& s) override { return s.size(); }
  int lengthOf(const char *p) { return strlen(p); }
};
static_assert(Interface<Impl>); // sanity check

template<Interface T>
void example(T& t) {
  Interface auto& interface = t;
  static_assert(std::same_as<decltype(interface), decltype(t)>);  // now both variables are Impl&
  int x = interface.lengthOf("hello world");  // does not construct a std::string
  int y = t.lengthOf("hello world");  // does not construct a std::string
}
int main() { Impl impl; example(impl); }

请注意,根本 无法恢复您在 base-class 方法中产生的“漏斗”效果。现在没有基础 class,interface 变量本身仍然是对 Impl 的静态引用,并且调用 lengthOf 将始终考虑 [=提供的完整重载集=19=]。这对性能来说是一件好事 — 我认为总的来说这是一件好事 — 但它与您的旧方法完全不同,所以,请小心!


对于您的 callback_A/B 示例,您的概念看起来像

template<class T, class A, class B>
concept Interface = requires (T& impl, const std::vector<A>& va) {
  { impl.callback_A(va) } -> std::same_as<A>;
  { impl.callback_B() } -> std::same_as<const B&>;
};

在现实生活中,我会非常强烈建议将那些 same_as 改为 convertible_to。但是这个代码已经很做作了,所以我们不用担心。

在 C++17 及更早版本中,等效的“概念”(type-trait) 定义如下所示 (complete working example in Godbolt)。在这里,我使用了一个宏 DV 来缩短样板文件;我在现实生活中不会那样做。

#define DV(Type) std::declval<Type>()

template<class T, class A, class B, class>
struct is_Interface : std::false_type {};

template<class T, class A, class B>
struct is_Interface<T, A, B, std::enable_if_t<
  std::is_same_v<int, decltype( DV(T&).callback_A(DV(const std::vector<A>&)) )> &&
  std::is_same_v<int, decltype( DV(T&).callback_B() )>
>> : std::true_type {};