松耦合隐式转换

发布于 2024-10-12 02:02:58 字数 1306 浏览 8 评论 0原文

当类型在语义上等效时,隐式转换非常有用。例如,想象两个库以相同的方式实现一个类型,但在不同的命名空间中。或者只是一个基本相同的类型,除了一些语义糖分。现在,您无法将一种类型传递到旨在使用另一种类型的函数(在这些库之一中),除非该函数是模板。如果不是,您必须以某种方式将一种类型转换为另一种类型。这应该是微不足道的(否则类型毕竟不那么相同!)但是显式调用转换会导致代码变得臃肿,并且大多数是无意义的函数调用。虽然此类转换函数实际上可能会复制一些值,但从高级“程序员”的角度来看,它们本质上什么也不做。

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

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

  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向量)。

Implicit conversion can be really useful when types are semantically equivalent. For example, imagine two libraries that implement a type identically, but in different namespaces. Or just a type that is mostly identical, except for some semantic-sugar here and there. Now you cannot pass one type into a function (in one of those libraries) that was designed to use the other, unless that function is a template. If it's not, you have to somehow convert one type into the other. This should be trivial (or otherwise the types are not so identical after-all!) but calling the conversion explicitly bloats your code with mostly meaningless function-calls. While such conversion functions might actually copy some values around, they essentially do nothing from a high-level "programmers" point-of-view.

Implicit conversion constructors and operators could obviously help, but they introduce coupling, so that one of those types has to know about the other. Usually, at least when dealing with libraries, that is not the case, because the presence of one of those types makes the other one redundant. Also, you cannot always change libraries.

Now I see two options on how to make implicit conversion work in user-code:

  1. The first would be to provide a proxy-type, that implements conversion-operators and conversion-constructors (and assignments) for all the involved types, and always use that.

  2. The second requires a minimal change to the libraries, but allows great flexibility:
    Add a conversion-constructor for each involved type that can be externally optionally enabled.

For example, for a type A add a constructor:

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

and a template

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

that disables the implicit conversion by default.

Then to enable conversion between two types, specialize the template:

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

and implement a convert function that can be found through ADL.

I would personally prefer to use the second variant, unless there are strong arguments against it.

Now to the actual question(s): What's the preferred way to associate types for implicit conversion? Are my suggestions good ideas? Are there any downsides to either approach? Is allowing conversions like that dangerous? Should library implementers in-general supply the second method when it's likely that their type will be replicated in software they are most likely beeing used with (I'm thinking of 3d-rendering middle-ware here, where most of those packages implement a 3D vector).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

盛夏尉蓝 2024-10-19 02:02:58

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

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

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

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

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

I'd prefer your "proxy" approach over other options, if I bothered with it at all.

Truth of the matter is that I've found this to be such a major problem in ALL spheres of development that I tend to steer clear of using any library specific construct outside of my interaction with that particular library. One example might be in dealing with events/signals in various different libraries. I've already chosen boost as something that is integral to my own project code so I quite purposefully use boost::signals2 for all communication within my own project code. I then write interfaces to the UI library I'm using.

Another example is strings. Every damn UI library out there reinvents the string. All of my model and data code uses the standard versions and I provide interfaces to my UI wrappers that work in such types...converting to the UI specific version only at that point where I'm interacting directly with a UI component.

This does mean that I can't leverage a lot of power provided by the various independent but similar constructs, and I'm writing a lot of extra code to deal with these conversions, but it's well worth it because if I find better libraries and/or need to switch platforms it becomes MUCH easier to do so since I haven't allowed these things to weed their way throughout everything.

So basically, I'd prefer the proxy approach because I'm already doing it. I work in abstract layers that distance me from any specific library I'm using and subclass those abstractions with the specifics required to interact with said library. I'm ALWAYS doing it, so wondering about some small area where I want to share information between two third party libraries is basically already answered.

合约呢 2024-10-19 02:02:58

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

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

自从我编写 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));

You could write a converter class (some proxy) that can implicitly convert from and to the incompatible types. Then you could use a the constructor to generate the proxy out of one of the types, and pass it to the method. The returned proxy would then be casted directly to the desired type.

The downside is that you have to wrap the parameter in all calls. Done right, the compiler will even inline the complete call without instantiating the proxy. And there is no coupling between the classes. Only the Proxy classes need to know them.

It's been a while since I've programmed C++, but the proxy woould be something like this:

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);
    }
}

The calls would always look like:

expectsType1(Proxy(type2));
expectsType1(Proxy(type1));
expectsType2(Proxy(type1));
她说她爱他 2024-10-19 02:02:58

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

一般来说,隐式转换有一个缺点,它可以做任何工作,因为它对那些对速度敏感的库用户来说是一种伤害(例如使用它 - 也许不知道)它——在内循环中)。当有多个不同的隐式转换可用时,它也可能导致意外行为。所以我想说,对于库实现者来说,一般来说,允许隐式转换是一个糟糕的建议。

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

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

Are there any downsides to either approach? Is allowing conversions like that dangerous? Should library implementers in-general supply the second method when...

In general, there is a downside to implicit conversion that does any work in that it's a disservice to those library users who are sensitive to speed (e.g. use it -- perhaps unaware of it -- in an inner loop). It can also cause unexpected behavior when several different implicit conversions are available. So I'd say it would be bad advice for library implementers in general to allow implicit conversions.

In your case -- essentially converting a tuple of numbers (A) to another tuple (B) -- that's so easy that a compiler can inline the conversion and maybe optimize it away entirely. So speed is not an issue. There might also not be any other implicit conversions to confuse things. So convenience may well win out. But the decision to provide implicit conversion should be taken on a case by case basis, and such cases would be rare.

A general mechanism like you suggest with the second variant would rarely be useful, and would make it easy to do some pretty bad things. Take this for an example (contrived but still):

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);
}

In this case, disabling the template constructor will change the value of B(20.0). In other words, merely by adding an implicit conversion constructor, you might change the interpretation of existing code. Obviously, that's very dangerous. So implicit conversion shouldn't be commonly available, but rather provided for very specific types, only when it's valuable and well-understood. It wouldn't be common enough to warrant your second variant.

To summarize: this would be better done outside of libraries, with full knowledge of all types to be supported. A proxy object seems perfect.

瑾夏年华 2024-10-19 02:02:58

关于您的第一个选择:

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

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

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

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

Regarding your first option:

Provide a proxy-type, that implements
conversion-operators and
conversion-constructors (and
assignments) for all the involved
types, and always use that.

You can use strings (text) as the proxy, if performance is not critical (or maybe if it is and the data are fundamentally strings anyway). Implement operators << and >> and you can use boost::lexical_cast<> to convert using a textual intermediate representation:

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

Obviously if you are very concerned about performance, you shouldn't do this, and there are other caveats too (both types should have sensible text representations), but it's fairly universal and "just works" with a lot of existing stuff.

丿*梦醉红颜 2024-10-19 02:02:58

您可以使用转换运算符重载吗?就像下面的例子一样:

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

Could you use converstion operator overloading? like in the following example:

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);
  }
};

Now these calls succeed:

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
不必在意 2024-10-19 02:02:58

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

I'm slow today. What was the problem with using the proxy pattern again? My advice, don't spend to much time worrying about copy functions doing unnecessary work. Also, explicit is good.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文