среда, 26 ноября 2014 г.

Скорее "филосовское". Почему синтаксический сахар в виде парадигмы property не востребован в классическом C++?

Ни на что не претендую. И ни к чему не призываю.

Просто размышляю.

Я не собираюсь делать из 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 стандарта". Только и всего :-)

В реальной жизни "именно это" я применять - никоим образом не собираюсь."

13 комментариев:

  1. тут должна быть картинка с буханкой хлеба, которую пытаются превратить в троллейбус. Да, но зачем?
    проперти довольно спорный синтаксический сахар, в С++, как и в Java, принято использовать геттеры и сеттеры и никто не страдает по этому поводу. Многие считают что проперти в языке вообще лишние. А то что на шаблонах можно извращаться - так это всем давно известно. В общем - не нужно.

    "Ну и собственно есть практический вопрос к аудитории - как мне правильно написать шаблонный класс, который использует лямбду, которая использует один из параметров шаблона?"
    template
    class SomeClass {
    std::function myF;
    ...
    };
    или в качестве сигнатуры функции можно использовать другой параметр шаблона, не прописывая явно их взаимодействие на уровне декларации типов.

    template
    class SomeClass {
    F myF;
    ...
    };

    Смешивание С++ и ObjC. В аду, должно быть, приготовлен отдельный котёл, для тех кто этим занимается.





















    ОтветитьУдалить
    Ответы
    1. скобочки в темплейтах похерились
      template(typename T)
      template(typename T, typename F)
      соотвествтенно

      Удалить
    2. "скобочки в темплейтах похерились"

      Ну собственно как я в посте и написал :-) Спасибо.

      Офигенскую книжку тут кстати прочитал. По мотивам разборок с шаблонами.

      "Шаблоны C++. Справочник разработчика.Дэвид Вандервуд. Николаи М. Джосатис".

      Удалить
    3. "или в качестве сигнатуры функции можно использовать другой параметр шаблона, не прописывая явно их взаимодействие на уровне декларации типов"

      -- вот меня это несколько и смутило, что сигнатура явно не задаётся. Оказывается - действительно - не задаётся.

      Есть понятие концепций, но его пока в стандарте C++ 11 нет. А жаль.

      Удалить
    4. " В аду, должно быть, приготовлен отдельный котёл, для тех кто этим занимается."

      Ну Джобс - уже наверное где-то там.

      А "по делу" - не соглашусь. Есть "миллионы строк кода" написанные на C++ и их очень дорого переписать на Objective-C, да и глупо иногда. Да и производительность иногда просаживается. А вот "смешать" - самое то. На мой вкус.

      Удалить
    5. "тут должна быть картинка с буханкой хлеба, которую пытаются превратить в троллейбус."

      Конечно :-) Я ровно так на это и смотрю.

      " Да, но зачем? "

      Я же написал - "чисто академический интерес".

      Я просто задался вопросом - "как я бы на месте Borland'а реализовывал бы __property не ломая компилятора и оставаясь в рамках ANSI стандарта". Только и всего :-)

      В реальной жизни "именно это" я применять - никоим образом не собираюсь.

      Удалить
  2. «Этот вопрос хорошо освещён тут -  Ссылка. Why doesn't C++ provide a "finally" construct?»
    -- Этого не видел. Повеселило :-) Спасибо.

    ОтветитьУдалить
    Ответы
    1. "Повеселило :-) Спасибо."
      -- вы не согласны с автором? :-)

      Удалить
    2. «вы не согласны с автором? :-)»
      -- Конечно не согласен :-)
      Если хотите, можем обсудить это в личке, поскольку тема - очевидный холливар.

      Удалить
    3. Пишите. С удовольствием почитаю.
      Хотя я лично с автором - согласен. Для Delphi finally - хорош. Для C++ - не хорош. Парадигмы разные.

      Пишите.

      Холивара кстати я в этом не вижу.

      Ибо я же с самого начала написал - "задавался вопросом - почему нет". Потом написал - "прочитал автора и он меня убедил".

      Т.е. я не "поверил в догму", а просто - "были сомнения", и "мне привели аргументы".

      Посему совсем не холивар.

      Просто - аргументы.

      С интересом прочитаю ваши.

      Может ваши - перевесят. А может и нет.

      Хотя почему лично? :-) Потом хоть можно будет опубликовать?

      Удалить
    4. Я прочитал ваше письмо. Меня даже как "закоренелого дельфиста" - не убедили. И не только меня.

      Удалить
  3. А может все проще, сахар property вводился только для публикации свойств в инспекторе объектов, нужно же было как то разделять свойства редактируемые визуально от невизуальных Понятно что без регистрации класса в IDE property нафиг не нужен.

    ОтветитьУдалить
    Ответы
    1. Тут вы правы. Но и это на C++ можно сделать по-другому.

      Удалить