#c #type-conversion #wrapper #primitive
#c #преобразование типов #оболочка #примитивный
Вопрос:
При разработке решения иногда может быть удобно предоставить классы-оболочки для примитивных типов данных. Рассмотрим класс, который представляет числовое значение, будь то a double
, a float
или an int
.
class Number {
private:
double val;
public:
Number(int n) : val(n) { }
Number(float n) : val(n) { }
Number(double n) : val(n) { }
// Assume copy constructors and assignment operators exist
Numberamp; add(const Numberamp; other) {
val = other.val;
return *this;
}
int to_int() const { return (int) val; }
float to_float() const { return (float) val; }
double to_double() const { return val; }
};
Теперь предположим, что у меня есть функция как таковая:
void advanced_increment(Numberamp; n) {
n.add(1);
}
И я бы использовал эту функцию как таковую:
Number n(2);
advanced_increment(n); // n = 3
Это звучит достаточно просто. Но что, если бы функция была такой?
void primitive_increment(intamp; n) {
n;
}
Обратите внимание, что приращение является примером. Предполагается, что функция будет выполнять более сложные операции над примитивными типами данных, которые они также должны быть способны выполнять над Number
типами без каких-либо проблем.
Как бы я мог использовать функцию точно так же, как раньше? Как в:
Number n(2);
primitive_increment(n);
Как я мог бы сделать мой Number
класс совместимым с primitive_increment
? Как я мог бы создать класс-оболочку для примитивных типов данных, который был бы совместим везде, где требуются эти типы данных?
Пока я нашел только два решения. Один из них заключается в создании такой функции, как doubleamp; Number::get_value()
а затем в использовании ее как primitive_increment(n.get_value());
. Второе решение заключается в создании неявных методов преобразования, таких как Number::operator intamp;()
; но это может привести ко многим неоднозначным вызовам и запутать код.
Мне интересно, есть ли какое-либо другое решение для реализации этих типов оболочек и сохранения их примитивной функциональности.
Обновить:
Для дальнейшего пояснения, в реальном проекте цель здесь состоит в том, чтобы сделать все типы данных производными от одного базового класса, который обычно упоминается как Object
при разработке такого решения. Ограничение заключается в том, что никакая внешняя библиотека не должна использоваться. Следовательно, если у меня есть контейнер, который имеет указатели на тип Object
, он должен быть способен хранить любое произвольное значение, примитивное или нет, и выполнять любую примитивную операцию, которая разрешена на Object
. Я надеюсь, что это объясняет это лучше.
Комментарии:
1. Что плохого в непосредственном использовании примитивных типов данных?
2. @Pubby, Ничего, но дизайн диктует такой подход для большей функциональности и инкапсуляции примитивных типов данных в сложные классы.
3. Взгляните на операторы Boost. Это удобно в подобных случаях.
4. @teedayf: Тогда, я думаю, вам нужен пример получше! Все зацикливаются на проблеме обработки преобразований между примитивными типами. Вам действительно нужен (например)
Int
класс, который можно использовать везде, где этоint
возможно?5. @teedayf: У меня есть очень сильное подозрение, что вам было бы гораздо лучше предоставить отдельные специализации или перегрузки для примитивов, а не пытаться втиснуть их в какую-то виртуальную полиморфную иерархию. Кто-то подумал, что это хорошая идея в 1995 году, и все, что мы получили от этого, — Java.
Ответ №1:
C 11 имеет явные перегрузки операторов.
struct silly_wrapper {
int foo;
explicit operator intamp;() { return foo; }
};
void primitive_increment(intamp; x) { x; }
int main()
{
silly_wrapper x;
primitive_increment(x); // works
x = 1; // doesn't work - can't implicitly cast
}
Комментарии:
1. Сложнее, чем кажется, поскольку он хочет, чтобы
foo
иbar
были равны.2. @MooingDuck Сложно? Я удалю
bar
— я показываю, как создавать явные перегрузки приведения, а не как создавать объединения.3. о, я пропустил слово
explicit
. Это меняет дело. Если этот вступительный текст там тоже был, я его тоже пропустил.
Ответ №2:
class Number {
enum ValType {DoubleType, IntType} CurType;
union {
double DoubleVal;
int IntVal;
};
public:
Number(int n) : IntVal(n), CurType(int) { }
Number(float n) : DoubleVal(n), CurType(DoubleType) { }
Number(double n) : DoubleVal(n), CurType(DoubleType) { }
// Assume copy constructors and assignment operators exist
Numberamp; add(const Numberamp; other) {
switch(CurType) {
case DoubleType: DoubleVal = other.to_double(); break;
case IntType: IntVal = other.to_int(); break;
}
return *this;
}
intamp; to_int() {
switch(CurType) {
case DoubleType: IntVal = DoubleVal; CurType = IntType; break;
//case IntType: DoubleVal = IntVal; CurType = DoubleType; break;
}
return IntVal;
}
const int to_int() const {
switch(CurType) {
case DoubleType: return (int)DoubleVal;
case IntType: return (int)IntVal;
}
}
const float to_float() const {
switch(CurType) {
case DoubleType: return (float)DoubleVal;
case IntType: return (float)IntVal;
}
}
doubleamp; to_double() {
switch(CurType) {
//case DoubleType: IntVal = DoubleVal; CurType = IntType; break;
case IntType: DoubleVal = IntVal; CurType = DoubleType; break;
}
return DoubleVal;
}
const double to_double() const {
switch(CurType) {
case DoubleType: return (double)DoubleVal;
case IntType: return (double)IntVal;
}
}
};
void primitive_increment(intamp; n) {
n;
}
int main() {
Number pi(3.1415);
primitive_increment(pi.to_int());
//pi now is 4
return 0;
}
Я признаю, что это довольно неудобная и не идеальная ситуация, но это решает данную проблему.
Комментарии:
1. Очень. Я думаю, что реальный ответ заключается в шаблоне функции.
Ответ №3:
Вместо того, чтобы предоставлять это primitive_increment
. Вам следует перегрузить оператор для вашего Number
класса и увеличить его таким образом.
Numberamp; operator () { val; return *this;}
Numberamp; operator =(const Numberamp; rhs) { val = rhs.Val; return *this;}
Number operator (const Numberamp; rhs) { Number t(*this); t =rhs; return t;}
смотрите: Операторы в C и C
Комментарии:
1. Однако функция увеличения — это просто пример. Предполагается, что эти функции будут выполнять другие сложные функции.
2. Действительно. Хотя соглашение заключается в том, чтобы определять
operator
в терминахoperator =
.3. @teedayf: Я думаю, что OP хотел знать, возможно ли перегружать операторы.
4. @MooingDuck, я прекрасно понимаю, что я мог бы перегрузить все допустимые операторы для класса-оболочки (на самом деле я бы сделал это независимо). Но это не решает вопрос, потому что я все равно не смог бы напрямую передать
Number
объектprimitive_increment
без проблем.5. @teedayf: Не понял, что целью было передать его функции, выполняющей
int
Ответ №4:
Если ваш Number
класс не реализует подмножество int
, вы просто не сможете этого сделать. Это дало бы неправильные результаты, если, например, ваш Number
класс содержит значение INT_MAX
и может также содержать значение INT_MAX 1
. Если ваш Number
класс моделирует подмножество int
, то преобразование в int
и обратно, конечно, является вариантом.
Кроме этого, ваш единственный шанс — переписать функцию для приема Number
объектов. В идеале сделайте это шаблоном, чтобы он мог работать как с int
, так и с Number
(а также с любым другим текущим или будущим классом, который представляет int
подобный интерфейс).
Ответ №5:
Сделайте оператор преобразования закрытым и попросите дружественную функцию выполнить преобразование внутри него.
class silly_wrapper {
private:
int foo;
float bar;
operator intamp;() { return foo; }
template <typename T>
friend void primitive_increment(Tamp; x) { static_cast<intamp;>(x); }
};
int main()
{
silly_wrapper x;
primitive_increment(x); // works
int i;
primitive_increment(i); // works
intamp; r = static_cast<intamp;>(x); // can't convert - operator is private
}
Комментарии:
1. Дело в том, что класс-оболочку необходимо передать произвольному набору функций.
Ответ №6:
Вот еще более причудливый ответ, о котором я только что подумал:
class Number;
template<class par, class base>
class NumberProxy {
base Val;
par* parent;
NumberProxy(par* p, base v) :parent(p), Val(v) {}
NumberProxy(const NumberProxyamp; rhs) :parent(rhs.parent), Val(rhs.Val) {}
~NumberProxy() { *parent = Val; }
NumberProxyamp; operator=(const NumberProxyamp; rhs) {Val = rhs.Val; return *this}
operator baseamp; {return Val;}
};
class Number {
private:
double val;
public:
Number(int n) : val(n) { }
Number(float n) : val(n) { }
Number(double n) : val(n) { }
// Assume copy constructors and assignment operators exist
int to_int() const { return (int) val; }
float to_float() const { return (float) val; }
double to_double() const { return val; }
NumberProxy<Number,int> to_int() { return NumberProxy<Number,int>(this,val); }
NumberProxy<Number,float> to_float() { return NumberProxy<Number,float>(this,val); }
NumberProxy<Number,double> to_double() { return NumberProxy<Number,double>(this,val); }
};
void primitive_increment(intamp; n) {
n;
}
int main() {
Number pi(3.1415);
primitive_increment(pi.to_int());
//pi now is 4
return 0;
}
Number.to_int()
возвращает a NumberProxy<int>
, который неявно преобразуется в intamp;
, с которым работает функция. Когда функция и выражение завершаются, временное NumberProxy<int>
хранилище уничтожается, и его деструктор обновляет родительское хранилище Number
с обновленным значением. Это имеет дополнительное удобство, заключающееся в том, что требуется лишь незначительная модификация Number
класса.
Очевидно, что здесь есть некоторая опасность, если вы вызываете to_N()
дважды в одном и том же операторе, два int amp; не будут синхронизированы, или если кто-то использует int amp; после окончания инструкции.
Комментарии:
1. Это тоже имело бы опасную семантику. Рассмотрим
Number x(3); int amp;r = x.to_int(); r = 4;
.2. Я забыл упомянуть об этом в ответе. Отредактировано.
Ответ №7:
(Это что-то вроде выстрела в темноте, поскольку я не совсем уверен, как ваш общий дизайн сочетается друг с другом.)
Как насчет шаблонных бесплатных функций:
class IncTagIntegral{};
class IncTagNonintegral{};
template <bool> struct IncTag { typedef IncTagNonintegral type; }
template <> struct IncTag<true> { typedef IncTagIntegral type; }
template <typename T> void inc_impl(T amp; x, IncTagIntegral)
{
x;
}
template <typename T> void inc_impl(T amp; x, IncTagNonintegral)
{
x = T(1);
}
template <typename T> void primitive_increment(T amp; x)
{
inc_impl<T>(x, typename IncTag<std::is_integral<T>::value>::type());
}
template <> void primitive_increment(Number amp; x)
{
// whatever
}
Этот подход может быть обобщен на другие функции, которые вам необходимо применить как к существующим типам, так и к вашим собственным типам.
Вот еще один перспективный вариант, на этот раз с использованием удаления типа:
struct TEBase
{
virtual void inc() = 0;
}
struct any
{
template <typename T> any(const T amp;);
void inc() { impl->inc(); }
private:
TEBase * impl;
};
template <typename T> struct TEImpl : public TEBase
{
virtual void inc() { /* implement */ }
// ...
}; // and provide specializations!
template <typename T> any::any<T>(const T amp; t) : impl(new TEImpl<T>(t)) { }
Ключ в том, что вы предоставляете различные конкретные реализации TEImpl<T>::inc()
с помощью специализации, но вы можете использовать a.inc()
для любого объекта a
типа any
. На основе этой идеи вы можете создать дополнительные оболочки со свободными функциями, например void inc(any amp; a) { a.inc(); }
.
Комментарии:
1. Его цель, похоже, состоит в том, чтобы передать свой
Number
класс произвольным функциям, ожидающимint
2. @MooingDuck: Вполне возможно. Я не могу сказать, что я полностью согласен с этим проектом. Приветствуется разработка OP и, возможно, предоставление более наглядного примера.
3. @KerrekSB, В реальном проекте цель здесь состоит в том, чтобы сделать все типы данных производными от одного базового класса, который обычно упоминается как
Object
при разработке такого решения. Ограничение заключается в том, что никакая внешняя библиотека не должна использоваться. Следовательно, если у меня есть контейнер, который имеет указатели на типObject
, он должен быть способен хранить любое произвольное значение, примитивное или нет, и выполнять любую примитивную операцию, которая разрешена наObject
. Я надеюсь, что это объясняет это лучше.4. @teedayf: Хм … это как бы объясняет один аспект того, что вы пытаетесь сделать (хотя и не то, почему это было бы хорошей идеей), но не то, как это связано с операторами и функциями. Вы каким-то образом хотите, чтобы все функции волшебным образом работали со всем, что получено из вашего суперобъекта?
5. @KerrekSB: Не волшебным образом, но в какой-то степени, да. Я хочу, чтобы это было невидимо для пользователя.