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);
}
问题:
这真的是首先要做的事情吗?我在网上看到好几篇解释概念的帖子,但总是很肤浅,我担心我会错过一些关于完美转发、移动等的东西...
我使用了 requires requires
子句来使函数参数接近它们的用法(在有许多方法时很有用)。然而,“接口”信息现在难以阅读:我们可以做得更好吗?
此外,Implem
实现接口的事实现在是“隐藏”在 class 中的部分。我们能否在不必使用 CRTP 编写另一个 class 或尽可能限制样板代码的情况下使更多的“public”?
我们可以在“mixin”部分做得更好吗PRNG_mt64
?理想情况下,将其转化为概念?
谢谢!
你的 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 {};
我曾经通过摘要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);
}
问题:
这真的是首先要做的事情吗?我在网上看到好几篇解释概念的帖子,但总是很肤浅,我担心我会错过一些关于完美转发、移动等的东西...
我使用了
requires requires
子句来使函数参数接近它们的用法(在有许多方法时很有用)。然而,“接口”信息现在难以阅读:我们可以做得更好吗?此外,
Implem
实现接口的事实现在是“隐藏”在 class 中的部分。我们能否在不必使用 CRTP 编写另一个 class 或尽可能限制样板代码的情况下使更多的“public”?我们可以在“mixin”部分做得更好吗
PRNG_mt64
?理想情况下,将其转化为概念?
谢谢!
你的 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 {};