Loosely coupled implicit conversion
Asked Answered
C

6

24

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

Crumpled answered 15/1, 2011 at 20:37 Comment(1)
Starting a bounty, since I still don't have any satisfying answers. Just to clarify, my question is strictly about how to implement implicit conversion safely and without the coupling of components that's usually involved. It is NOT about other ways to deal with 3rd-party types, or how to do explicit conversion conveniently.Crumpled
J
7

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.

Jacklighter answered 15/1, 2011 at 21:47 Comment(3)
It actually sounds like (please correct me if I misunderstood!) you are working around the issue by wrapping other libraries with a thin layer of your own code. While that's totally fine, it just concentrates the glue-code in a module. However, that can be a lot of code for larger third party libraries! I'd actually like to get rid of as much glue-code as possible.Crumpled
@Crumpled If you're using third party libraries, almost by definition you don't have control over the API and will be required to use some sort of glue code to get the data back into a normalized form for use by your application. Consolidating the glue in one area makes it easier to maintain.Aprylapse
@Mark B: Yea, that's very true. But I don't see how it relates to my question, or my comment. I agree that having all the glue in one area makes it easier to maintain, but less code is also easier to maintain than a lot of code (given similar levels of complexity). You can have both with those approaches (unless I missed something, which is why I'm asking), while writing a wrapper module is just working around the problem, but is a lot less flexible and a lot more code.Crumpled
G
1

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));
Gourley answered 28/1, 2011 at 13:32 Comment(7)
What's the advantage to explicit conversion functions that you overload?Crumpled
You can always use this approach without getting concerned whether it's type 1 with library 2 or vice versa. Even passing the right type can be handled. Additionally you can add some verification code etc.Gourley
Ok, I see your point. It's actually a clever/practical implementation of my proxy idea. I don't see why you would have to wrap your parameters in the proxy-ctor call every time tho. It seems like they could also be called implicitly.Crumpled
Wrapping is neccessary because you would never change IncompatibleType1 or IncompatibleType2. Thus you must somehow convert from them to the Proxy class to allow implicit conversion to one of them.Gourley
Why not? IIRC, one additional conversion is allowed to match target types (can someone with knowledge of the standard help out?). So the conversion chain would be IncompatibleType1->Proxy->IncompatibleType2 and all conversions could be implicit.Crumpled
See https://mcmap.net/q/492584/-c-implicit-conversions Only one implicit conversion is allowed.Gourley
However, this means that only the second approach can use existing types without coupling them. Using the proxy, you cannot enable implicit conversion between two 3rd party classes at all. I'm surprised that noone pointed that out yet.Crumpled
S
1

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.

Significative answered 31/1, 2011 at 5:55 Comment(2)
Ok, it seems like you're kinda getting what I'm after here. However, I don't like your example and the conclusion you are deriving. You can do the same "dangerous" stuff by adding a broken to-conversion (an implicit constructor) or from-conversion (a conversion operator) to the proxy type. Also, one could argue that the implicit conversion from 'float' is actually the problem there, even if convert wasn't broken.You're right that you should only add implicit conversion for types where you don't change the meaning.Crumpled
I agree that the dangers are still there; but with a proxy type you'd be aware that you are using a "looser" type. In the example I gave (assuming your proposal with templated constructor), someone could have used B(20.0) without type A even existing. Later, someone could add type A and enable the implicit conversion, and the breakage would be in code that has nothing to do with A.Significative
B
0

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.

Batson answered 15/1, 2011 at 22:39 Comment(3)
No, performance wasn't any of my concerns. Implicit conversion however, was. The goal with using a proxy-type is to avoid having to explicitly convert to anything, be it with lexical_cast or anything else. My proxy proposal is about implementing MyProxy::MyProxy(const OtherType&) and MyProxy::operator OtherType() to be able to just pass/receive things to/from library functions without cluttering my code with (trivial) conversion-calls.Crumpled
Well, on the one hand it may seem not nice to conversion routines littering your code. On the other hand, future maintainers might not think so highly of your desire to have so much implicit behavior. It's not so clear who would be right in a general sense.Batson
I completely agree that this is dangerous when abused. But when used right, it can keep your code cleaner. Anyways, my question was not about the righteousness of implicit conversions, but how to implement them without having one of the involved types depend on the other (without coupling!).Crumpled
D
0

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
Delegate answered 28/1, 2011 at 14:39 Comment(2)
I reread your question and figured that this is not a solution to your problem. However it could help in conjunction with you proxy approach, by adding constructors for all types and casting operators to all typesDelegate
Yes, in fact, that's exactly what I meant by the proxy approach!Crumpled
B
-2

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.

Barrens answered 30/1, 2011 at 2:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.