Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Creating a compatible String object

So I have an existing library that provides a string type.

It implicitly converts to-from C style strings like so:

struct TypeIDoNotOwn {
  TypeIDoNotOwn() {}
  TypeIDoNotOwn(TypeIDoNotOwn const&) {}
  TypeIDoNotOwn(char const*) {}

  TypeIDoNotOwn& operator=(TypeIDoNotOwn const&) {return *this;}
  TypeIDoNotOwn& operator=(char const*) {return *this;}

  operator char const*() const {return nullptr;}
};

it has other methods, but I do not think they are important. These methods have bodies, but my problem doesn't involve them, so I have stubbed them out.

What I want to do is to create a new type that can be used relatively interchangably with the above type, and with "raw string constants". I want to be able to take an instance of TypeIDoNotOwn, and replace it with TypeIDoOwn, and have code compile.

As an example, this set of operations:

void test( TypeIDoNotOwn const& x ) {}

int main() {
  TypeIOwn a = TypeIDoNotOwn();
  TypeIDoNotOwn b;
  a = b;
  b = a;
  TypeIOwn c = "hello";
  TypeIDoNotOwn d = c;
  a = "world";
  d = "world";
  char const* e = a;
  std::pair<TypeIDoNotOwn, TypeIDoNotOwn> f = std::make_pair( TypeIOwn(), TypeIOwn() );
  std::pair<TypeIOwn, TypeIOwn> g = std::make_pair( TypeIDoNotOwn(), TypeIDoNotOwn() );
  test(a);
}

If I replace TypeIOwn with TypeIDoNotOwn above, it compiles. How do I get it to compile with TypeIOwn without modifying TypeIDoNotOwn? And without having to introduce any casts or changes other than the change-of-type at point of declaration?

My first attempt looks somewhat like this:

struct TypeIOwn {
  TypeIOwn() {}
  operator char const*() const {return nullptr;}
  operator TypeIDoNotOwn() const {return {};}
  TypeIOwn( TypeIOwn const& ) {}
  TypeIOwn( char const* ) {}
  TypeIOwn( TypeIDoNotOwn const& ) {}
  TypeIOwn& operator=( char const* ) {return *this;}
  TypeIOwn& operator=( TypeIOwn const& ) {return *this;}
  TypeIOwn& operator=( TypeIDoNotOwn const& ) {return *this;}
};

but I get a series of ambiguous overloads:

 main.cpp:31:4: error: use of overloaded operator '=' is ambiguous (with operand types 'TypeIDoNotOwn' and 'TypeIOwn')
         b = a;
         ~ ^ ~
 main.cpp:9:17: note: candidate function
         TypeIDoNotOwn& operator=(TypeIDoNotOwn const&) {return *this;}
                        ^
 main.cpp:10:17: note: candidate function
         TypeIDoNotOwn& operator=(char const*) {return *this;}

and

 /usr/include/c++/v1/utility:315:15: error: call to constructor of 'TypeIDoNotOwn' is ambiguous
             : first(_VSTD::forward<_U1>(__p.first)),
               ^     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 main.cpp:40:51: note: in instantiation of function template specialization 'std::__1::pair<TypeIDoNotOwn, TypeIDoNotOwn>::pair<TypeIOwn, TypeIOwn>' requested here
       std::pair<TypeIDoNotOwn, TypeIDoNotOwn> f = std::make_pair( TypeIOwn(), TypeIOwn() );
                                                   ^
 main.cpp:7:7: note: candidate constructor
       TypeIDoNotOwn(TypeIDoNotOwn const&) {}
       ^
 main.cpp:8:7: note: candidate constructor
       TypeIDoNotOwn(char const*) {}
       ^

In my "real" code I have other operators, like += and ==, that have similar problems.

The scope of the real problem is large; millions of lines of code, and I want to swap out TypeIDoNotOwn for TypeIOwn at many thousands of locations, but not at many hundreds of others. And at thousands of locations they interact in a way that causes the conversion ambiguity.

I have solved the problem of a function taking TypeIDoNotOwn& at the 100s of spots where it happens by wrapping it with a macro that creates a temporary object that creates a TypeIDoNotOwn from the TypeIOwn, returns a reference to that, then when the temporary object is destroyed copies it back to the TypeIOwn. I want to avoid having to do a similar sweep to handle ==, +=, =, copy-construction, and similar situations.

Live example.

If I try to remove the operator TypeIDoNotOwn to clear up that ambiguity, other cases where the conversion need occur don't work right (as it requires 2 user-defined constructions to get from TypeIOwn to TypeIDoNotOwn), which then requires an explicit conversion to occur (at many 100s or 1000s of locations)

If I could make one conversion look worse than the other, it would work. Failing that, I could try fixing the non-operator= and copy-construct cases by overloading a free TypeIDoNotOwn == TypeIOwn operator with exact matching (and similar for other cases), but that doesn't get me construction, function calls, and assignment.

like image 807
Yakk - Adam Nevraumont Avatar asked Apr 20 '17 14:04

Yakk - Adam Nevraumont


Video Answer


2 Answers

With the usual caveats that this is C++ and there's bound to be some clever workaround... no.


Let's go through your use cases. You want both copy initialization and copy assignment to work:

TypeIOwn a = ...;
TypeIDoNotOwn b = a;  // (*)
TypeIDoNotOwn c;
c = a;                // (*)

That necessitates:

operator TypeIDoNotOwn();

If you just provided operator const char*(), then assignment would work, but copy-initialization would fail. If you provided both, it's ambiguous as there's no way to force one conversion to be preferred to the other (the only real way to force conversion ordering would be to create type hierarchies, but you can't inherit from const char* so you can't really force that to work).

Once we get ourselves down to having just the one conversion function, the only code that doesn't work from the list of examples is:

const char* e = a; // error: no viable conversion

At which point, you'll have to add a member function:

const char* e = a.c_str();

Both pair constructions work fine with the one conversion function. But just by process of elimination, we can't have both.

like image 52
Barry Avatar answered Sep 28 '22 06:09

Barry


There's no magic bullet, but you might get some improvement by declaring the conversion from TypeIOwn to TypeIDoNotOwn as explicit.

explicit operator TypeIDoNotOwn() const { return{}; }

This means you do have to make a change at each spot where this happens, but it does resolve the problem with "const char*" being equally valid for assignments. Is it worth the trade-off? You'll have to decide.

However, for incrementally changing the code base, I have had some luck in similar situations using a different strategy. I just set a #define flag and compile using entirely one or the other, and I can continue to code normally with TypeIDoNotOwn, while simultaneously making progress on making everything work with TypeIDoOwn.

#ifdef SOME_FLAG
struct TypeIOwn {...};
typedef TypeIOwn TypeIDoNotOwn;
#else
struct TypeIDoNotOwn {...};
#endif

You will have to test both for every update, until you finally make the plunge.

Since you say this is a string class, also consider the option of moving towards std::string, so your TypeIOwn becomes a thin wrapper for std::string, and no longer provides an implicit conversion to const char*. Instead, provide data(). You no longer have ambiguous conversions from TypeIOwn -> (const char* | TypeIDoNotOwn) -> TypeIDoNotOwn, because like std::string, you no longer allow implicit conversion to const char*, and any work you put into making the code work with this will pay off when you ditch both string classes entirely and use std::string.

like image 42
Kenny Ostrom Avatar answered Sep 28 '22 05:09

Kenny Ostrom