【问题标题】:Making identical C++ type aliases incompatible使相同的 C++ 类型别名不兼容
【发布时间】:2016-12-23 11:13:18
【问题描述】:

我将std::vector<int> 用于两种不同的信息。我想确保我不会意外混合使用这两种用途。

简而言之,我希望这段代码失败:

#include <vector>

using A = std::vector<int>;
using B = std::vector<int>;

void fa(const A&);
void fb(const B&);

void fun()
{
    A ax;
    B bx;

    fa(bx);
    fb(ax);
}

即使fa 需要A 类型的参数,此代码也可以编译。 显然AB 是相同的。

让这段代码正确编译的最简单方法是什么:

fa(ax);
fb(bx);

并使这段代码失败:

fa(bx);
fb(ax);

当然,我可以将std::vector&lt;int&gt; 包装在另一个类中,但是我需要重写它的接口。或者,我可以从 std::vector&lt;int&gt; 继承,但通常不鼓励这样做。

简而言之,我需要两个不兼容的std::vector&lt;int&gt; 版本。

编辑

有人建议Strong typedefs可以解决这个问题。这只是部分正确。如果我使用BOOST_STRONG_TYPEDEF(std::vector&lt;int&gt;, A),我需要添加一些烦人的演员表。例如,而不是

A ax{1,3,5};

我需要使用

A ax{std::vector<int>{1,3,5}};

而不是

for (auto x : ax) ...

我需要使用

for (auto x : (std::vector<int>)ax) ...

【问题讨论】:

  • 与其将整个std::vector&lt;int&gt; 包装在另一个类中,不如将int 包装成更有意义的东西呢?
  • Strong typedefs的可能重复
  • 你能给我们一些背景信息吗?通常我会回答特定于 A 和 B 应该是什么的事情。也就是说,经常使用 std::vector 会创建不应该插入到函数其他部分的向量,这很明显,对吧?就像定义不应插入排序算法的顺序的向量一样。所以,我真的不明白为什么你需要它们不兼容。当然,你会有一些理由,我真的很想听听他们的意见。它们可能是您问题的关键。
  • @Aziuth,向量用于保存游戏中位置的两种不同表示。一种表示对某些计算有用,另一种表示对其他计算有用。我想确保不使用表示 B 调用期望表示 A 的函数。
  • “或者,我可以从 std::vector 继承,但经常不鼓励这样做。” Pfft,这里没有错。你不打算动态分配你的类型,或者向它添加数据成员,是吗?不,所以只需从 std::vector&lt;int&gt; 公开继承来创建自己的类型并完成它。

标签: c++ c++11


【解决方案1】:

我认为你想要的仍然是最好的实现:

struct A : public std::vector<int>{
  using vector::vector;
};
struct B : public std::vector<int>{
  using vector::vector;
};

它完全符合您的要求。没有理由想出一些丑陋的黑客来避免一个干净的声明。我看到这种子类型不受欢迎的主要原因是相同的事物应该表现得像它们一样并且可以互换使用。但这正是您想要抑制的,因此对它进行子类型化正是您想要的声明:它们具有相同的接口,但它们不应该使用相同,因为它们不一样。

【讨论】:

  • @Aziuth:没有。 X 类型是 Y iff X is_a Y 类型的后代。两种类型是相等的,只要其中任何一个 is_an 其他。 OP明确指出AB是不同的:Ais_not_aB(即使你可以用相同的参数指定它们的状态,它们的原理图也不同)。
  • @Aziuth: 向量不是多态的,所以任何派生类都不能作为向量替代,因为向量方法不会被重定向到继承类。因此,任何与 OOP 相关的口头禅(derive = is_a,如果它不是虚拟的就不要派生等),不能在这里应用,因为我们不是在纯 OOP 上下文中(我们在通用编程领域,这里) . A 和 B 都衰减为向量的事实仅意味着采用向量的函数也可以与 A 和 B 一起使用(但可能是他需要的)。但是需要 A 的函数不适用于 B 或向量。
  • @Aziuth:我们明白你在说什么。我们说你错了。 A 确实为向量添加了功能:成为A 的功能。而已。问题解决了。没有更多的工作要做。
  • @Aziuth 恕我直言,这正是我们在编程中应该做的事情。充分利用类型检查器是一件非常好的事情,它可以让我们发现许多否则会被忽视的错误。
  • @Aziuth 在具有更强大类型系统的语言中这样做实际上很常见(想到 OCaml)。没有理由我可以将指定温度的整数传递给期望整数表示时间(以分钟为单位)的函数 - 这只是一个不重要的实现细节,它们都由相同的原始数据类型表示,但它们的含义不同并且它们不应该可以互换。
【解决方案2】:

不管怎样,这是primitive obsession 的情况。 ints 真正代表某事,vectors 是该某事的集合,或者vector&lt;int&gt;s 代表某事。

在这两种情况下,都应该通过将原语包装成更有意义的东西来解决这个问题。例如:

class column
{
  int id;
  /*...*/
}; 
class row
{
  int id;
  /*...*/
};

std::vector&lt;row&gt;std::vector&lt;column&gt; 不能互换。

当然,如果vector&lt;int&gt; 是真正具有其他含义的原语,那么同样的想法也可以应用于vector&lt;int&gt; 而不是int

【讨论】:

  • 使用大量代码对基本整数类型进行子类型化以避免对向量本身进行子类型化只会解决问题。如果这是可以接受的,为什么不能接受对向量进行子类型化?
  • @Christoph 因为整数很可能代表不同的事物。它们恰好存储在向量中。这种primitive obsession 应该被修复。
  • 我喜欢这个的第一部分,但不喜欢第二部分。只需使用模板:template&lt;class T&gt; class tagged_integer { /* ... */ }; using row=tagged_integer&lt;struct row_tag&gt;;
  • 你所谓的“原始痴迷”实际上是我们为解决多个库(分别发布且不相关)必须相互通信的设计问题提出的一个非常好的解决方案。根据std::function&lt;&gt; 和“恰到好处”的基本类型指定接口,为我们提供了用于互连的空调解器,而不是 O(n^2) 调解器或要依赖的基库库。此外,它还允许客户端混合搭配版本的库。
  • @lorro:API 边界是一种特殊情况……但它们只是边界。任何切断边界的东西都应该 (1) 验证并 (2) 转换为适合域的类型......理想情况下,一步完成,因为验证是关于建立不变量。
【解决方案3】:

或者,我可以从 std::vector 继承,但通常不鼓励这样做。

IMO,这取决于情况。总的来说可能是一个很好的解决方案

#include <vector>

class VectorA :
    public std::vector<int> {
 public:
  VectorA() = default;
  ~VectorA() = default;
  VectorA(const VectorA&) = default;
  VectorA(VectorA&&) = default;
  VectorA& operator=(const VectorA&) = default;
  VectorA& operator=(VectorA&&) = default;
};

class VectorB :
    public std::vector<int> {
 public:
  VectorB() = default;
  ~VectorB() = default;
  VectorB(const VectorB&) = default;
  VectorB(VectorB&&) = default;
  VectorB& operator=(const VectorB&) = default;
  VectorB& operator=(VectorB&&) = default;
};

你仍然可以使用VectorAVectorB作为法向量,但是你不能在它们之间切换。

void acceptA(const VectorA& v) {
  // do something
}

void acceptB(const VectorB& v) {
  // do something
}

template<typename T>
void acceptVector(const std::vector<T>& v) {
  // do something
}

int main(int argc, char *argv[]) {
  VectorA va;
  VectorB vb;

  acceptA(va);  // you can only pass VectorA
  acceptB(vb);  // same here for VectorB

  acceptVector(va);  // any vector
  acceptVector(vb);

  return 0;
}

【讨论】:

  • 将所有内容声明为默认值的目的是什么?放手吧!
【解决方案4】:

这也是您可以在 C++ 中进行面向对象编程以及重用库类型进行基于对象的编程的部分原因。

创建 A 和 B 类来模拟您的域中的行为。如果这两种行为都是用整数向量的字段实现的,这并不重要。只要不破坏封装,对不同向量的所有操作都将在其类的范围内,不会发生混淆。

#include <vector>

class A {
    std::vector<int> cake_orders_;

public:
    void f() ; // can only do something to do with cake
};

class B {
    std::vector<int> meal_worm_lengths_;

public:
    void f() ; // can only do something to do with worms
};


void fun()
{
    A ax;
    B bx;

    a.f(); // has to be the right thing
    b.f();
}

【讨论】:

    猜你喜欢
    • 2018-11-15
    • 1970-01-01
    • 2020-08-01
    • 2011-07-07
    • 1970-01-01
    • 2020-08-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多