Создание совместимого объекта String

Итак, у меня есть существующая библиотека, которая предоставляет тип строки.

Он неявно преобразует строки строки стиля C в так:

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

у него есть другие методы, но я не думаю, что они важны. У этих методов есть тела, но моя проблема не связана с ними, поэтому я их исчерпал.

Что я хочу сделать, так это создать новый тип, который можно использовать относительно взаимозаменяемо с указанным выше типом и с "raw string constants". Я хочу иметь возможность взять экземпляр TypeIDoNotOwn и заменить его на TypeIDoOwn, и компилировать код.

В качестве примера этот набор операций:

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

Если я заменил TypeIOwn на TypeIDoNotOwn выше, он скомпилируется. Как мне его скомпилировать с помощью TypeIOwn без изменения TypeIDoNotOwn? И без необходимости вводить какие-либо отбрасывания или изменения, кроме изменения типа в точке объявления?

Моя первая попытка выглядит примерно так:

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

но я получаю серию неоднозначных перегрузок:

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

и

 /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*) {}
       ^

В моем "реальном" коде у меня есть другие операторы, такие как += и ==, которые имеют схожие проблемы.

Объем реальной проблемы большой; миллионы строк кода, и я хочу поменять TypeIDoNotOwn для TypeIOwn на многих тысячах мест, но не на многих сотнях других. И в тысячах мест они взаимодействуют таким образом, что вызывает неоднозначность преобразования.

Я решил проблему функции, принимающей TypeIDoNotOwn& в 100s пятен, где это происходит, обернув ее макросом, который создает временный объект, который создает TypeIDoNotOwn из TypeIOwn, возвращает ссылку на что тогда, когда временный объект уничтожается, копирует его обратно в TypeIOwn. Я хочу избежать необходимости выполнять аналогичную развертку для обработки ==, +=, =, копирования-построения и подобных ситуаций.

Живой пример.

Если я попытаюсь удалить operator TypeIDoNotOwn, чтобы устранить эту неоднозначность, в других случаях, когда требуется преобразование, не работает правильно (поскольку для получения от TypeIOwn до TypeIDoNotOwn требуется 2 пользовательских построения), который затем требует явного преобразования (на многих 100 или 1000 мест)

Если бы я мог сделать одно преобразование хуже, чем другое, это сработало бы. В противном случае я мог бы попытаться исправить случаи не operator= и copy-construct, перегрузив бесплатный оператор TypeIDoNotOwn == TypeIOwn с точным сопоставлением (и аналогичным для других случаев), но это не создает мне конструкцию, вызовы функций и назначение.

Ответ 1

С обычными оговорками, что это С++, и там должно быть какое-то умное обходное решение... нет.


Пройдите через ваши прецеденты. Вы хотите, чтобы и инициализация копирования, и назначение копии работали:

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

Это требует:

operator TypeIDoNotOwn();

Если вы предоставили operator const char*(), тогда назначение будет работать, но инициализация с копией завершится с ошибкой. Если вы предоставили оба варианта, это двусмысленно, поскольку нет способа заставить одно преобразование быть предпочтительным для другого (единственным реальным способом принудительного упорядочения конверсий будет создание иерархии типов, но вы не можете наследовать от const char*, чтобы вы могли 'действительно заставить работать).

Как только мы перейдем к использованию только одной функции преобразования, единственным кодом, который не работает из списка примеров, является:

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

В этот момент вам нужно будет добавить функцию-член:

const char* e = a.c_str();

Обе конструкции pair отлично работают с одной функцией преобразования. Но просто по мере устранения мы не можем иметь обоих.

Ответ 2

Нет волшебной пули, но вы можете получить некоторое улучшение, объявив преобразование из TypeIOwn в TypeIDoNotOwn как явное.

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

Это означает, что вам нужно внести изменения в каждое место, где это происходит, но оно разрешает проблему с "const char *", которая одинаково применима для назначений. Стоит ли компромисс? Вы должны решить.

Однако для постепенного изменения базы кода мне повезло в подобных ситуациях, используя другую стратегию. Я просто установил флаг #define и скомпилировал его полностью или полностью, и я могу продолжать нормально кодировать TypeIDoNotOwn, одновременно добиваясь прогресса в том, чтобы все работало с TypeIDoOwn.

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

Вам нужно будет протестировать оба для каждого обновления, пока вы, наконец, не решитесь.

Поскольку вы говорите, что это строковый класс, также рассмотрите возможность перехода к std::string, поэтому ваш TypeIOwn станет тонкой оболочкой для std::string и больше не обеспечивает неявное преобразование в const char *. Вместо этого укажите данные(). У вас больше нет двусмысленных преобразований из TypeIOwn → (const char * | TypeIDoNotOwn) → TypeIDoNotOwn, потому что, как и std::string, вы больше не допускаете неявного преобразования в const char *, и любая работа, которую вы вкладываете в создание работа с этим кодом окупится, когда вы полностью отделите оба строковых класса и используйте std::string.