Ни на что не претендую. И ни к чему не призываю.
Просто размышляю.
Я не собираюсь делать из C++ Delphi или наоборот. Всё написанное ниже представляет лишь "академический интерес".
Почему
синтаксический сахар в виде парадигмы
property не востребован в классическом C++?
Я вот совсем
не сторонник холиваров. Я стремлюсь мыслить
трезво.
И если у меня возникает вопрос - "почему такая-то синтаксическая конструкция востребована в языке A, но не востребована в языке B", то я пытаюсь найти ответ не в плоскости "религиозных войн", а в смысле различий между языком A и языком B.
Например меня
долгое время мучил вопрос - "почему в C++ нет конструкции try..finally".
Но поскольку C++ разрабатывали более чем умные люди, то я сразу подозревал, что это "не просто так".
Этот вопрос хорошо освещён тут -
Ссылка. Why doesn't C++ provide a "finally" construct?
Также меня долгое время мучил вопрос - "почему в C++ нету парадигмы property".
Особенно учитывая тот факт, что в расширениях C++ Builder'а эта парадигма - есть.
Но опять же - поскольку C++ разрабатывали более чем умные люди, то я подозревал, что это "не просто так".
И вот к каким выводам я не так давно пришёл.
Потому, что в C++ есть собственный "синтаксический сахар" в виде ссылок на значения, шаблонов и переопределения операторов.
Переопределения операторов "бездумно" -
я не люблю, но при вдумчивой разработке библиотек - они очень и очень востребованы.
Итак.
Пусть у нас есть класс SomeClass на C++:
class SomeClass
{
public:
std::string a;
}; // SomeClass
И его использование:
SomeClass X;
std::string Y = X.a;
X.a = "SomeString";
- ну тут всё понятно.
Есть - класс
SomeClass.
У него есть член данных
a - его можно писать и читать.
Теперь задумаемся о
инкапсуляции данных.
Как "бедные" программисты на C++ (на взгляд Delphi программистов) обычно "вынуждены" писать?
Ну как-то так:
class SomeClass
{
private:
std::string m_a;
public:
const std::string get_a () const
{
return m_a;
}
void set_a (const std::string & aValue)
{
m_a = aValue;
}
}; // SomeClass
И его использование:
SomeClass X;
std::string Y = X.get_a();
X.set_a("SomeString");
Сразу "бросается в глаза"
копирование значения.
Это легко исправить, вернув ссылку (std::string &), а не значение (std::string):
class SomeClass
{
private:
std::string m_a;
public:
const std::string & get_a () const
{
return m_a;
}
void set_a (const std::string & aValue)
{
m_a = aValue;
}
}; // SomeClass
Тут на сцену выходят ссылки. Они - тут главные.
Но пока на время забудем о них.
Немного потеоретизируем на тему "как было бы хорошо иметь property в C++".
Ну скажем так:
class SomeClass
{
private:
std::string m_a;
private:
const std::string get_a () const
{
return m_a;
}
void set_a (const std::string & aValue)
{
m_a = aValue;
}
public:
property std::string a
read get_a
write set_a;
}; // SomeClass
И его использование:
SomeClass X;
std::string Y = X.a;
X.a = "SomeString";
Красиво? Красиво.
Но зададимся вопросом - "зачем ?".
Вспомним про нашу триаду - ссылки, шаблоны, переопределения операторов.
А также вспомним о
перегрузке функций.
Перепишем наш класс так:
class SomeClass
{
private:
std::string m_a;
public:
const std::string a () const
// - это getter
{
return m_a;
}
std::string & a ()
// - это setter
{
return m_a;
}
}; // SomeClass
И его использование:
SomeClass X;
std::string Y = X.a();
X.a() = "SomeString";
Красиво? По-моему - тоже красиво.
Ну, а лишние скобочки - () это "не вопрос".
Всё?
Не совсем.
Встаёт вопрос.
А если мы хотим написать setter в виде:
void set_a (const std::string & aValue)
{
if (this->someCondition(aValue))
m_a = aValue;
}
- что тогда делать?
Ну во-первых тогда можно "поступиться красотой" и оставить метод-setter, а не метод возвращающий неконстантую ссылку.
Такая возможность у нас всегда есть:
class SomeClass
{
private:
std::string m_a;
public:
const std::string a () const
// - это getter
{
return m_a;
}
void set_a (const std::string & aValue)
// - это setter
{
if (this->someCondition(aValue))
m_a = aValue;
}
}; // SomeClass
И его использование:
SomeClass X;
std::string Y = X.a();
X.set_a("SomeString");
- но как сделать "красиво"?
Тут вспомним о шаблонах и перегрузке операторов,
Напишем вспомогательный шаблонный класс ValueSetter:
template<class DataOwner, typename DataType>
class ValueSetter
{
private:
DataType & m_data;
DataOwner * m_owner;
public
ValueSetter (DataOwner * aOwner, DataType & aData) : m_data(aData), m_owner(aOwner)
// - инициализирующий конструктор
{
}
ValueSetter (const ValueSetter & anOther) : m_data(anOther.m_data), m_owner(anOther.m_owner)
// - копирующий конструктор
{
}
ValueSetter & operator= (const DataType & aValue)
// - оператор присваивания
{
if (m_owner->someCondition(aValue))
m_data = aValue;
return *this;
}
} ; // ValueSetter
; //template
И тогда наш класс приобретает вид:
class SomeClass
{
private:
std::string m_a;
public:
const std::string a () const
// - это getter
{
return m_a;
}
typedef ValueSetter<SomeClass, std::string> Setter_for_a;
Setter_for_a & a ()
// - это setter
{
Setter_for_a vSetter(this, a);
return vSetter;
}
}; // SomeClass
И его использование:
SomeClass X;
std::string Y = X.a();
X.a() = "SomeString";
Теперь - как параметризовать someCondition?
Приведу ссылку -
std::sort.
Давайте немного перепишем класс ValueSetter (тут я могу путаться в синтаксисе, но надеюсь, что смысл понятен):
template<class DataOwner, typename DataType, typename Condition>
class ValueSetter
{
private:
DataType & m_data;
DataOwner * m_owner;
public
ValueSetter (DataOwner * aOwner,DataType & aData) : m_data(aData), m_owner(aOwner)
// - инициализирующий конструктор
{
}
ValueSetter (const ValueSetter & anOther) : m_data(anOther.m_data), m_owner(anOther.m_owner)
// - копирующий конструктор
{
}
ValueSetter & operator= (const DataType & aValue)
// - оператор присваивания
{
if (Condition(m_owner, aValue))
m_data = aValue;
return *this;
}
} ; // ValueSetter
; //template
И наш класс приобретает вид:
class SomeClass
{
private:
std::string m_a;
static bool condition_for_a (const SomeClass & aThis, const std::string & aValue)
{
return aThis.someCondition(aValue);
}
public:
const std::string a () const
// - это getter
{
return m_a;
}
typedef ValueSetter<SomeClass, std::string, condition_for_a> Setter_for_a;
Setter_for_a & a ()
// - это setter
{
Setter_for_a vSetter(this, a);
return vSetter;
}
}; // SomeClass
А ещё ведь можно использовать
лямбды.
C++11, передача Lambda-выражения как параметра.
Лямбда выражения теперь и в С++.
C++0x (С++11). Лямбда-выражения.
Могу что-то путать, но тогда наш ValueSetter приобретает вид:
template<typename DataType, typename Condition>
class ValueSetter
{
private:
DataType & m_data;
public
ValueSetter (DataType & aData) : m_data(aData)
// - инициализирующий конструктор
{
}
ValueSetter (const ValueSetter & anOther) : m_data(anOther.m_data)
// - копирующий конструктор
{
}
ValueSetter & operator= (const DataType & aValue)
// - оператор присваивания
{
if (Condition(aValue))
m_data = aValue;
return *this;
}
} ; // ValueSetter
; //template
И класс SomeClass приобретает вид:
class SomeClass
{
private:
std::string m_a;
public:
const std::string a () const
// - это getter
{
return m_a;
}
typedef ValueSetter<std::string, [&] (const std::string & aValue) ->bool { return this->someCondition(aValue); }> Setter_for_a;
Setter_for_a & a ()
// - это setter
{
Setter_for_a vSetter(a);
return vSetter;
}
}; // SomeClass
Понятно, что "буков больше", чем при использовании парадигмы
property, но я
специально привожу всё как есть.
Чтобы было понятно "как оно работает".
Ведь далее связку m_a и const std::string a () с Setter_for_a & a () можно упаковать в ещё один шаблон.
И вынести все эти шаблоны в "стандартную библиотеку".
Было бы желание.
Ведь чем силён C++? Именно стандартными библиотеками и возможностью написания своих. С шаблонизацией. Те. мета-программированием. Т.е. с внесением "собственного синтаксического сахара".
Насчёт передачи функций и лямбд в шаблон - повторюсь, могу путать -посему критика на сей счёт - приветствуется. Пишу дома, где компилятора C++ - нет. Завтра на работе - проверю. Если будет время.
Ну и собственно есть практический вопрос к аудитории - как мне
правильно написать шаблонный класс, который использует лямбду, которая использует один из параметров шаблона?
В этом вопросе - я пока "плаваю". Буду пытаться компилировать код.
Ну и коротко - если мы хотим "индексированные свойства", то что делать?
А всё просто:
class SomeClass1
{
private:
std::vector<std::string> m_a;
public:
const std::string & a () const
{
return m_a;
}
std::string & a ()
{
return m_a;
}
}; // SomeClass1
И использование:
SomeClass1 X;
std::string Y = X.a()[0];
X.a()[1] = "Some Value";
Ну и если на SomeClass1 определить оператор [] вот так:
class SomeClass1
{
private:
std::vector<std::string> m_a;
public:
const std::string & a () const
{
return m_a;
}
std::string & a ()
{
return m_a;
}
const std::string & operator[] (size_t anIndex) const
{
return m_a[anIndex];
}
std::string & operator[] (size_t anIndex)
{
return m_a[anIndex];
}
}; // SomeClass1
- то можно написать и так:
SomeClass1 X;
std::string Y = X[0];
X[1] = "Some Value";
Или даже так:
SomeClass1 X;
char Y = X[0][1];
X[1][0] = 'c';
Ну а "всё остальное" - тоже через ValueSetter.
Вот собственно и всё пока.
Надеюсь, что кому-нибудь понравятся мои размышления.
А теперь об интересе не академическом, а вполне себе практическом.
В следующей заметке я напишу, как путём примерно такой же техники wraper'ов и holder'ов заменить использование std::string на NSString *, без перелопачивания "тонн кода".
Опять же лишь за счёт триады - ссылки, шаблоны, перегрузка операторов.
Спросите - зачем?
Отвечу ссылкой -
Коротко. Про STL под iOS/MacOS.
Просто у нас было какое-то количество алгоритмов работающих с std::string и прочими объектами STL, которые "захотелось" "малой кровью" переделать на NSObject, NSArray, NSString, NSData и NSDistionary.
Ну не то, чтобы прям захотелось, просто "жизнь приказала".
Но на сегодня - пока всё.
P.S. Ну и в тему "синтаксического сахара" -
Скорее "для себя". Классы в скриптовой машине.
P.P.S.
http://programmingmindstream.blogspot.ru/2014/11/property-c.html?showComment=1417552354575#c8460879686358984674
"Я просто задался вопросом - "как я бы на месте Borland'а реализовывал бы __property не ломая компилятора и оставаясь в рамках ANSI стандарта". Только и всего :-)
В реальной жизни "именно это" я применять - никоим образом не собираюсь."