开发者

Loosely coupled implicit conversion

开发者 https://www.devze.com 2023-02-04 05:03 出处:网络
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

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 b开发者_C百科oost::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).


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.


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


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.


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.


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


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.

0

精彩评论

暂无评论...
验证码 换一张
取 消