Слабосвязанное неявное преобразование
Неявное преобразование может быть действительно полезным, когда типы семантически эквивалентны. Например, представьте две библиотеки, которые реализуют тип одинаково, но в разных пространствах имен. Или просто тип, который в основном идентичен, за исключением некоторого семантического сахара здесь и там. Теперь вы не можете передать один тип в функцию (в одной из этих библиотек), которая была разработана для использования другого, если только эта функция не является шаблоном. Если это не так, вы должны каким-то образом преобразовать один тип в другой. Это должно быть тривиально (или, в конце концов, типы не так уж и идентичны!), Но вызов преобразования явно увеличивает ваш код в основном бессмысленными вызовами функций. Хотя такие функции преобразования могут на самом деле копировать некоторые значения, они, по сути, ничего не делают с точки зрения "программистов" высокого уровня.
Очевидно, что конструкторы и операторы неявного преобразования могут помочь, но они вводят связь, поэтому один из этих типов должен знать о другом. Обычно, по крайней мере, когда речь идет о библиотеках, это не так, потому что наличие одного из этих типов делает другой избыточным. Кроме того, вы не можете всегда менять библиотеки.
Теперь я вижу два варианта, как заставить неявное преобразование работать в коде пользователя:
Первый заключается в предоставлении прокси-типа, который реализует операторы преобразования и конструкторы преобразования (и назначения) для всех задействованных типов, и всегда использует это.
Второй требует минимального изменения библиотек, но обеспечивает большую гибкость: добавьте конструктор преобразования для каждого задействованного типа, который может быть дополнительно включен извне.
Например, для типа 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_ {};
и реализовать convert
функция, которую можно найти через ADL.
Я лично предпочел бы использовать второй вариант, если нет веских аргументов против него.
Теперь к актуальным вопросам: каков предпочтительный способ связать типы для неявного преобразования? Мои предложения хорошие идеи? Есть ли недостатки у любого подхода? Опасно ли такое преобразование? Если разработчики библиотек вообще предоставляют второй метод, когда существует вероятность, что их тип будет реплицирован в программном обеспечении, с которым они, скорее всего, используются (я имею в виду промежуточное программное обеспечение для 3D-рендеринга здесь, где большинство этих пакетов реализуют 3D вектор).
6 ответов
Я бы предпочел ваш подход "прокси", а не другие варианты, если бы вообще его беспокоил.
Правда в том, что я обнаружил, что это настолько серьезная проблема во ВСЕХ сферах разработки, что я склонен избегать использования какой-либо конкретной библиотечной конструкции вне моего взаимодействия с этой конкретной библиотекой. Одним из примеров может быть работа с событиями / сигналами в различных библиотеках. Я уже выбрал boost как что-то, что является неотъемлемой частью моего собственного кода проекта, поэтому я довольно целенаправленно использую boost::signal2 для всех коммуникаций в моем собственном коде проекта. Затем я пишу интерфейсы в библиотеку пользовательского интерфейса, которую я использую.
Другой пример - строки. Каждая чертова библиотека пользовательского интерфейса заново изобретает строку. Вся моя модель и код данных используют стандартные версии, и я предоставляю интерфейсы для моих оболочек пользовательского интерфейса, которые работают с такими типами... преобразование в конкретную версию пользовательского интерфейса только в тот момент, когда я непосредственно взаимодействую с компонентом пользовательского интерфейса.
Это означает, что я не могу использовать много возможностей, предоставляемых различными независимыми, но схожими конструкциями, и я пишу много дополнительного кода, чтобы справиться с этими преобразованиями, но это того стоит, потому что, если я найду лучшие библиотеки и / или необходимость переключения платформ становится намного проще, так как я не позволил этим вещам проложить себе путь во всем.
В общем, я бы предпочел прокси-подход, потому что я уже делаю это. Я работаю в абстрактных слоях, которые отделяют меня от какой-либо конкретной библиотеки, которую я использую, и подклассируем эти абстракции со спецификой, необходимой для взаимодействия с указанной библиотекой. Я ВСЕГДА этим занимаюсь, поэтому мне интересно узнать о какой-то небольшой области, где я хочу делиться информацией между двумя сторонними библиотеками.
Вы можете написать класс конвертера (некоторый прокси), который может неявно преобразовывать несовместимые типы и обратно. Затем вы можете использовать конструктор для генерации прокси из одного из типов и передать его методу. Возвращенный прокси затем будет приведен к нужному типу.
Недостатком является то, что вы должны обернуть параметр во всех вызовах. Если все сделано правильно, компилятор даже встроит полный вызов, не создавая прокси. И нет никакой связи между классами. Только прокси-классы должны знать их.
Прошло много времени с тех пор, как я запрограммировал 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));
Есть ли недостатки у любого подхода? Опасно ли такое преобразование? Должны ли разработчики библиотеки вообще предоставлять второй метод, когда...
В общем, есть обратная сторона неявного преобразования, которое выполняет какую-либо работу, потому что это плохая услуга для тех пользователей библиотеки, которые чувствительны к скорости (например, используют ее - возможно, не подозревая об этом - во внутреннем цикле). Это также может вызвать неожиданное поведение, когда доступны несколько различных неявных преобразований. Поэтому я бы сказал, что для разработчиков библиотек в целом было бы плохим советом разрешать неявные преобразования.
В вашем случае - по сути, преобразование кортежа чисел (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). Другими словами, просто добавив неявный конструктор преобразования, вы можете изменить интерпретацию существующего кода. Очевидно, это очень опасно. Таким образом, неявное преобразование не должно быть общедоступным, а должно предоставляться для очень специфических типов, только когда оно ценно и понятно. Это не будет достаточно распространенным, чтобы оправдать ваш второй вариант.
Подводя итог: это было бы лучше сделать за пределами библиотек, с полным знанием всех типов, которые будут поддерживаться. Прокси-объект кажется идеальным.
Не могли бы вы использовать перегрузку оператора разговора? как в следующем примере:
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
Что касается вашего первого варианта:
Предоставьте прокси-тип, который реализует операторы преобразования и конструкторы преобразования (и присваивания) для всех задействованных типов, и всегда используйте это.
Вы можете использовать строки (текст) в качестве прокси, если производительность не критична (или, может быть, если это так, и данные в любом случае являются в основном строками). Реализуйте операторы <<
а также >>
и вы можете использовать boost::lexical_cast<>
преобразовать с помощью текстового промежуточного представления:
const TargetType& foo = lexical_cast<TargetType>(bar);
Очевидно, что если вы очень обеспокоены производительностью, вы не должны этого делать, и есть другие предостережения (оба типа должны иметь разумные текстовые представления), но это довольно универсально и "просто работает" со многими существующими вещами.
Я медленный сегодня. В чем была проблема с использованием шаблона прокси снова? Мой совет, не тратьте много времени на беспокойство о том, что функции копирования выполняют ненужную работу. Кроме того, явное это хорошо.