C++中只允许精确类型(不允许子类)

axkjgtzd  于 2023-07-01  发布在  其他
关注(0)|答案(4)|浏览(125)

可以声明类型,只允许类而不允许任何子类(我知道这违反了Liskov substitution principle,但我仍然想知道是否有方法做到这一点)。
比如说

#include <iostream>

struct A {};
struct B : A {};

void fn(/*I want this to be only A, not subclasses*/A arg) {
    // do stuff with arg
    std::cout << "fn called";
}

int main() {
    A a;
    fn(a);
    B b;
    fn(b);  // should raise compile-time error here
}

我希望fn(b)给予一个编译时错误。
代码链接:https://wandbox.org/permlink/AiLkHwp5rg7AD7gf

nzk0hqpo

nzk0hqpo1#

你可以使用std::same_as的概念:

#include <concepts>

void fn(std::same_as<A> auto arg)  {
    std::cout << "fn called";
}
46qrfjad

46qrfjad2#

您可以删除函数

template<typename T>
void fn(T arg)=delete;
#include <iostream>

struct A {};
struct B : A {};

void fn(A arg) {
    // do stuff with arg
    std::cout << "fn called";
}

template<typename T>
void fn(T arg)=delete;

int main() {
    A a;
    fn(a);
    B b;
    fn(b);  // should raise compile-time error here
}

因为模板匹配所有内容,所以在重载解析中总是考虑它。由于非模板比模板更受欢迎,因此fn(A{});是比模板更好的候选者,另一方面,fn(B{});与模板完全匹配,而非模板则需要隐式转换。
当然,您不能禁止某人在调用之前将B转换为A,因此解决方案充其量是脆弱和混乱的。

bf1o4zei

bf1o4zei3#

您可以禁止其他重载:

void fn(A arg) {
    // do stuff with arg
    std::cout << "fn called";
}

template <typename T> void fn(T) = delete;
rfbsl7qr

rfbsl7qr4#

有很多方法可以做到这一点,所以让我们比较一下:

约束(C++20起)

#include <concepts>

// abbreviated function template, could also write:
//   template <std::same_as<A> T>
void fn(std::same_as<A> auto arg)  {
    std::cout << "fn called";
}

诊断

<source>:8:6: note: candidate template ignored: constraints not satisfied [with arg:auto = B]
void fn(std::same_as<A> auto arg)  {
     ^
<source>:8:9: note: because 'std::same_as<B, A>' evaluated to false
void fn(std::same_as<A> auto arg)  {
        ^

此选项有效,因为auto在此处推导为B,这与A不同。该约束不关心B是否可以转换为A(尽管std::convertible_to可以)。

优缺点

  • 简短
  • 相比之下,错误稍显冗长
  • 需要相当新的C++版本
  • 用代码清楚地表达意图

std::enable_if和SFINAE(C++11起)

// note: convenience aliases is_same_v and enable_if_t are not available in C++11 yet
//       this solution is C++17
template <typename T>
auto fn(T arg) -> std::enable_if_t<std::is_same_v<T, A>> {
    std::cout << "fn called";
}

诊断

<source>:9:6: note: candidate template ignored: requirement 'std::is_same_v<B, A>'
                    was not satisfied [with T = B]
auto fn(T arg) -> std::enable_if_t<std::is_same_v<T, A>> {
     ^

这个实现与C++20版本基本相同,我们只是通过SFINAE而不是约束来实现它。

优缺点

  • 比C++20的对应部分更冗长,但可以忍受
  • 在C++11中解决方案要丑陋得多,因为没有方便的别名
  • 只有clang有std::enable_if的诊断 * 这个好 *,其他编译器将产生更差的输出
  • 与前一种解决方案一样,明确传达了意图

删除函数(C++11起)

void fn(A arg) {
    std::cout << "fn called";
}

template <typename T>
void fn(T arg) = delete;

诊断

<source>:14:6: note: candidate function [with T = B] has been explicitly deleted
void fn(T arg) = delete;
     ^
<source>:8:6: note: candidate function
void fn(A arg) {
     ^

我们可以声明任何函数为deleted,调用它会使程序变得不规范。此解决方案之所以有效,是因为fn(T)在重载解决方面胜出,因为不需要从BT的隐式转换,只需要从BA

优缺点

  • 需要函数重载
  • = delete在任意函数上是令人惊讶的,它不是一个众所周知的功能
  • 意图没有在代码中清楚地传达,我们必须将重载集作为一个整体来看待以理解它
  • 没有标准库依赖性,即对编译速度有潜在的好处,而且非常可移植

static_assert(C++11起)

template <typename T>
void fn(T arg) {
    // or std::is_same_v in C++17
    static_assert(std::is_same<T, A>::value, "fn must be called with A");
    std::cout << "fn called";
}

诊断

<source>:10:5: error: static assertion failed due to requirement 'std::is_same<B, A>::value':
                      fn must be called with A
    static_assert(std::is_same<T, A>::value, "fn must be called with A");
    ^

这是一个非常简单但有效的解决方案。我们检查给定的类型是否实际上是A。不会考虑从BA的隐式转换。

优缺点

  • 调用站点没有错误(IDE支持不好,通常不显示红色下划线)
  • 非常明确的诊断,可由我们定制,以便我们可以传达意图

总结

错误的质量对于每个解决方案都是好的,至少在使用clang时是这样。您更喜欢哪种解决方案部分取决于您的需求,以及您希望与哪个版本的C++兼容。然而,个人偏好也在这里发挥作用,因为没有一个解决方案在每个方面都比其他解决方案更好。

相关问题