【问题标题】:Loosely coupled implicit conversion松耦合隐式转换
【发布时间】:2011-01-15 20:37:32
【问题描述】:

当类型在语义上等价时,隐式转换非常有用。例如,假设两个库实现了相同的类型,但在不同的命名空间中。或者只是一种几乎相同的类型,除了一些语义糖在这里和那里。现在,您不能将一种类型传递给旨在使用另一种类型的函数(在其中一个库中),除非该函数是模板。如果不是,您必须以某种方式将一种类型转换为另一种类型。这应该是微不足道的(否则类型毕竟不是那么相同!)但是显式调用转换会使您的代码膨胀,其中大部分是无意义的函数调用。虽然这样的转换函数实际上可能会复制一些值,但从高级“程序员”的角度来看,它们基本上什么都不做。

隐式转换构造函数和运算符显然会有所帮助,但它们会引入耦合,因此其中一种类型必须了解另一种类型。通常,至少在处理库时,情况并非如此,因为其中一种类型的存在使另一种变得多余。此外,您不能总是更改库。

现在我看到了两个关于如何在用户代码中进行隐式转换的选项:

  1. 第一个是提供代理类型,为所有涉及的类型实现转换运算符和转换构造函数(和赋值),并始终使用它。

  2. 第二个需要对库进行最小的更改,但具有很大的灵活性: 为每个涉及的类型添加一个转换构造函数,可以选择在外部启用。

例如,为类型 A 添加构造函数:

template <class T> A(
  const T& src,
  typename boost::enable_if<conversion_enabled<T,A>>::type* ignore=0
)
{
  *this = convert(src);
}

还有一个模板

template <class X, class Y>
struct conversion_enabled : public boost::mpl::false_ {};

默认禁用隐式转换。

然后要启用两种类型之间的转换,请专门化模板:

template <> struct conversion_enabled<OtherA, A> : public boost::mpl::true_ {};

并实现一个可以通过ADL找到的convert函数。

我个人更喜欢使用第二种变体,除非有强烈的反对意见。

现在回到实际问题:为隐式转换关联类型的首选方法是什么?我的建议是好主意吗?这两种方法都有缺点吗?允许这样的转换危险吗?当他们的类型很可能在他们最有可能使用的软件中被复制时,库实现者一般是否应该提供第二种方法(我在这里考虑 3d 渲染中间件,其中大多数包都实现了 3D向量)。

【问题讨论】:

  • 开始赏金,因为我仍然没有任何令人满意的答案。澄清一下,我的问题是关于如何安全地实现隐式转换并且没有通常涉及的组件的耦合。这不是关于处理 3rd-party 类型的其他方法,或者如何方便地进行显式转换。

标签: c++ boost-mpl enable-if


【解决方案1】:

如果我愿意的话,我更喜欢你的“代理”方法而不是其他选项。

事实上,我发现这是所有开发领域中的一个重大问题,以至于我倾向于避免在与该特定库的交互之外使用任何库特定构造。一个例子可能是处理各种不同库中的事件/信号。我已经选择 boost 作为我自己项目代码的integral 的东西,所以我非常有目的地使用 boost::signals2 来在我自己的项目代码中进行所有通信。然后我将接口写入我正在使用的 UI 库。

另一个例子是字符串。每个该死的 UI 库都在重新发明字符串。我的所有模型和数据代码都使用标准版本,并且我为我的 UI 包装器提供了在此类类型中工作的接口......仅在我直接与 UI 组件交互的那一点转换为 UI 特定版本。

这确实意味着我无法利用各种独立但相似的构造提供的大量功能,并且我正在编写大量额外的代码来处理这些转换,但这是非常值得的,因为如果我发现更好的库和/或需要切换平台,这样做变得容易得多,因为我没有让这些东西在所有事情中都杂乱无章。

所以基本上,我更喜欢代理方法,因为我已经在这样做了。我在抽象层中工作,使我与我正在使用的任何特定库保持距离,并使用与所述库交互所需的细节对这些抽象进行子类化。我一直在这样做,所以想知道我想在两个第三方库之间共享信息的一些小区域基本上已经得到解答。

【讨论】:

  • 它实际上听起来像(如果我误解了,请纠正我!)你正在通过用你自己的代码的薄层包装其他库来解决这个问题。虽然这完全没问题,但它只是将胶水代码集中在一个模块中。但是,对于较大的第三方库来说,这可能是很多代码!我实际上想尽可能多地摆脱胶水代码。
  • @ltjax 如果您使用第三方库,几乎按照定义,您无法控制 API,并且需要使用某种胶水代码将数据恢复为标准化供您的应用程序使用的表格。将胶水固定在一个区域更容易维护。
  • @Mark B:是的,这是真的。但我不明白它与我的问题或我的评论有何关系。我同意将所有的粘合剂集中在一个区域可以更容易维护,但更少的代码也比大量的代码更容易维护(考虑到类似的复杂程度)。你可以同时使用这些方法(除非我错过了一些东西,这就是我要问的原因),而编写一个包装模块只是解决这个问题,但灵活性要低得多,代码要多得多。
【解决方案2】:

你可以编写一个转换器类(一些代理),它可以隐式地从不兼容的类型转换为不兼容的类型。然后,您可以使用构造函数从其中一种类型中生成代理,并将其传递给方法。然后将返回的代理直接转换为所需的类型。

缺点是您必须在所有调用中包装参数。如果做得对,编译器甚至会内联完整的调用,而无需实例化代理。并且类之间没有耦合。只有代理类需要知道它们。

我已经有一段时间没有编写 C++ 了,但代理应该是这样的:

class Proxy { 
  private:
    IncompatibleType1 *type1;
    IncompatibleType2 *type2;
    //TODO static conversion methods
  public:
    Proxy(IncompatibleType1 *type1) {
      this.type1=type1;
    }
    Proxy(IncompatibleType2 *type2) {
      this.type2=type2;
    }
    operator IncompatibleType1 * () { 
      if(this.type1!=NULL)
        return this.type1;
      else
        return convert(this.type2);
    }
    operator IncompatibleType2 * () { 
      if(this.type2!=NULL)
        return this.type2;
      else
        return convert(this.type1);
    }
}

调用总是如下所示:

expectsType1(Proxy(type2));
expectsType1(Proxy(type1));
expectsType2(Proxy(type1));

【讨论】:

  • 重载的显式转换函数有什么好处?
  • 您始终可以使用这种方法,而不必担心它是类型 1 和库 2,反之亦然。即使传递正确的类型也可以处理。此外,您可以添加一些验证码等。
  • 好的,我明白你的意思了。这实际上是我的代理想法的一个聪明/实用的实现。我不明白为什么每次都必须将参数包装在 proxy-ctor 调用中。似乎它们也可以被隐式调用。
  • 包装是必要的,因为您永远不会更改 IncompatibleType1 或 IncompatibleType2。因此,您必须以某种方式将它们转换为 Proxy 类,以允许隐式转换为其中之一。
【解决方案3】:

这两种方法有什么缺点吗?允许这样的转换危险吗?一般情况下,库实现者是否应该提供第二种方法...

一般来说,隐式转换有一个缺点,它不利于那些对速度敏感的图书馆用户(例如,在内部循环中使用它——也许没有意识到它)。当有几个不同的隐式转换可用时,它也可能导致意外行为。所以我会说对于库实现者一般而言允许隐式转换是一个不好的建议。

在您的情况下——本质上是将一个数字元组 (A) 转换为另一个元组 (B)——这非常容易,编译器可以内联转换并可能完全优化它。所以速度不是问题。也可能没有任何其他隐式转换来混淆事物。因此,便利很可能会胜出。但提供隐式转换的决定应视具体情况而定,这种情况很少见。

像您建议的第二个变体的一般机制很少有用,并且可以很容易地做一些非常糟糕的事情。以这个为例(做作但仍然):

struct A {
    A(float x) : x(x) {}
    int x;
};

struct B {
    B(int y): y(y) {}
    template<class T> B(const T &t) { *this = convert(t); }
    int y;
};

inline B convert(const A &a) {
    return B(a.x+1);
}

在这种情况下,禁用模板构造函数将更改 B(20.0) 的值。换句话说,仅仅通过添加一个隐式转换构造函数,你可能会改变对现有代码的解释。显然,这是非常危险的。因此,隐式转换不应该是普遍可用的,而是为非常特定的类型提供,只有当它有价值且易于理解时。保证您的第二个变体并不常见。

总结:最好在库之外完成,充分了解要支持的所有类型。代理对象看起来很完美。

【讨论】:

  • 好吧,看来你有点得到我想要的东西了。但是,我不喜欢你的例子和你得出的结论。您可以通过将损坏的 to-conversion(隐式构造函数)或 from-conversion(转换运算符)添加到代理类型来执行相同的“危险”操作。此外,有人可能会争辩说,从“浮点”进行的隐式转换实际上是那里的问题,即使转换没有被破坏。你是对的,你应该只为不改变含义的类型添加隐式转换。
  • 我同意危险仍然存在;但是对于代理类型,您会知道您使用的是“宽松”类型。在我给出的示例中(假设您的提议带有模板构造函数),即使没有 A 类型,也有人可以使用 B(20.0)。后来,有人可以添加类型 A 并启用隐式转换,而破坏将出现在与 A 无关的代码中。
【解决方案4】:

关于您的第一个选项:

提供一个代理类型,实现 转换运算符和 转换构造函数(和 作业)为所有相关人员 类型,并始终使用它。

您可以使用字符串(文本)作为代理,如果性能不重要(或者可能是,并且数据基本上是字符串)。实现运算符&lt;&lt;&gt;&gt;,您可以使用boost::lexical_cast&lt;&gt; 使用文本中间表示进行转换:

const TargetType& foo = lexical_cast<TargetType>(bar);

显然,如果您非常关心性能,则不应该这样做,并且还有其他警告(两种类型都应该具有合理的文本表示),但它相当普遍,并且“适用于”很多现有的东西.

【讨论】:

  • 不,性能不是我关心的问题。 隐式 转换却是。使用代理类型的目标是避免显式转换为任何内容,无论是lexical_cast 还是其他任何内容。我的代理提案是关于实现 MyProxy::MyProxy(const OtherType&amp;)MyProxy::operator OtherType() 以便能够只向库函数传递/接收东西,而不会因(微不足道的)转换调用而使我的代码混乱。
  • 好吧,一方面,转换例程乱扔代码似乎不太好。另一方面,未来的维护者可能不会对你有这么多隐性行为的愿望有那么高的评价。在一般意义上,谁是正确的还不清楚。
  • 我完全同意这在滥用时很危险。但是如果使用得当,它可以让你的代码更干净。无论如何,我的问题不是关于隐式转换的正确性,而是如何在不涉及其中一种类型的情况下实现它们依赖于另一种(没有耦合!)。
【解决方案5】:

你能使用转换运算符重载吗?就像下面的例子:

class Vector1 {
  int x,y,z;
public:
  Vector1(int x, int y, int z) : x(x), y(y), z(z) {}
};

class Vector2 {
  float x,y,z;
public:
  Vector2(float x, float y, float z) : x(x), y(y), z(z) {}

  operator Vector1()  {
    return Vector1(x, y, z);
  }
};

现在这些调用成功了:

void doIt1(const Vector1 &v) {
}

void doIt2(const Vector2 &v) {
}

Vector1 v1(1,2,3);
Vector2 v2(3,4,5);
doIt1(v1);
doIt2(v2);

doIt1(v2); // Implicitely convert Vector2 into Vector1

【讨论】:

  • 我重读了您的问题,并认为这不是您问题的解决方案。但是,它可以与您的代理方法结合使用,通过为所有类型添加构造函数并将运算符转换为所有类型
  • 是的,事实上,这正是我所说的代理方法的意思!
【解决方案6】:

我今天很慢。再次使用代理模式有什么问题?我的建议是,不要花太多时间担心复制功能会做不必要的工作。另外,显式也不错。

【讨论】:

    猜你喜欢
    • 2017-05-09
    • 1970-01-01
    • 1970-01-01
    • 2018-11-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-06-05
    • 1970-01-01
    相关资源
    最近更新 更多