25

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

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

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

  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_ {};

并实现一个convert可以通过ADL找到的功能。

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

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

4

6 回答 6

7

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

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

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

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

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

于 2011-01-15T21:47:42.597 回答
1

这两种方法都有缺点吗?允许这样的转换危险吗?图书馆实施者是否应该在一般情况下提供第二种方法......

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

在您的情况下 - 基本上将一个数字元组(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) 的值。换句话说,仅仅通过添加一个隐式转换构造函数,你可能会改变对现有代码的解释。显然,这是非常危险的。因此,隐式转换不应该普遍可用,而是为非常特定的类型提供,只有在它有价值且易于理解时才提供。保证您的第二个变体并不常见。

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

于 2011-01-31T05:55:51.863 回答
1

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

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

自从我编写 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));
于 2011-01-28T13:32:49.177 回答
0

你可以使用转换运算符重载吗?如下例所示:

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
于 2011-01-28T14:39:46.287 回答
0

关于您的第一个选项:

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

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

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

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

于 2011-01-15T22:39:21.027 回答
-2

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

于 2011-01-30T02:13:31.043 回答