Информация об изменениях

Сообщение Re: Не могу понять ссылки в C++ от 15.06.2024 10:00

Изменено 15.06.2024 10:19 rg45

Re: Не могу понять ссылки в C++
Здравствуйте, Worminator X, Вы писали:

WX>Несколько раз пытался освоить C++, и всегда забрасывал это дело — сложный, совершенно непонятный язык.


WX>Вот, допустим, совершенно тривиальный код обхода списка целых чисел на чистом Си с указателями:


WX>Можно переписать его в стиле "Си с классами", здесь пока тоже все очевидно, просто теперь new/delete вместо malloc/free и class вместо struct:


WX>Примерно так я представлял себе C++ в школе и универе, не зная ни про ссылки, ни про операторы, ни про шаблоны и имея смутное представление о множественном наследовании.

WX>Т.к. Pure C, Turbo Pascal и Delphi (и чуть позже Java с C#) решали все необходимые задачи, изучать C++ тогда не было никакой необходимости.

WX>По правилам хорошего тона в C++ нужно всегда использовать ссылки вместо указателей (кроме интеграции с кодом на Си) и по возможности не управлять памятью вручную, т.к. это чревато утечками.

WX>Я попытался переписать код, и вышло примерно следующее (для упрощения пока без шаблонов):
WX>Помимо того, что этот талмуд совершенно непонятен, оно еще и не работает.
WX>Рекурсивный обход списка в recursivePrintIntegerList вроде выводит все как положено, но в foreachPrintIntegerList цикл почему-то выводит одни единицы и не хочет переходить на следующий элемент.
WX>Что нужно исправить? И что можно почитать по теме?
WX>Из учебника Столярова так и не понял, что такое есть ссылка в C++ (вроде синтаксический сахар над указателем, но как-то странно и непонятно работает).
WX>И еще, почему, если в EmptyList сделать пустой конструктор, то на строки const EmptyList empty(); const IntegerList n3(3, empty); компилятор ругается? Вариант с const EmptyList empty(void) не помог.

Я бы начал с того, что нет в С++ такого правила, которое запрещало бы использование сырых указателей. Плохо, когда владение объектом осуществляется через сырой указатель — это действительно чревато и утечками памяти, и риском влететь на неопределенное поведение в случае повторного удаления объекта. Также плохо, когда указатель используется в качестве ссылки на обязательный объект, который не может отсутсвовать. Вообще плохо, когда сырые указатели используются бездумно, просто потому, что программист не видит особой разницы между ссылкой и указателем. В то же время есть случаи, когда использование сырых указателей вполне оправдано — например, в качестве итераторов последовательностей, занимающих непрерывные области в памяти.

Идем далее. Современный С++ заточен, в первую очередь, под решение практических задач. На выбранном примере трудно увидеть преимущества С++ как раз ввиду того, что практическая ценность данного примера стремится к нулю. Суди сам: твой список заточен под конкретный тип данных и у тебя (в рамках данного примера) даже не предусмотрено даже сколько-нибудь общего способа наполнить список произвольными данными. Как только ты захочешь исправить этот недостаток, твой код тут же начнет раздуваться и обрастать костылями, подобными print_integer_list и new_integer_list. Короче говоря, твой оригинальный пример на С лишь создает иллюзию простоты в виду своей полной практической бесполезности.

Идем дальше, твой пример на C иллюстрирует следующую большую проблему — необходимость ручного управления ресурсами. Программист, разрабатывавший класс списка тупо переложил ответственность за освобождение ресурсов на пользователя. Теперь остается только молиться на то, что пользователь вызовет free_integer_list в нужное время, в нужном месте и сделает это ровно один раз. Когда вся программа строится по такому принципу, вот тогда и получаются традиционные для С проблемы: утечки памяти, крэши и неопределенное поведение.

Что еще хочется отметить — абсолютно необоснованное использование полиморфизма времени выполнения — очень распространненная болячка. Мне доводилось видеть много примеров, когда ран-тайм полиморфизм применялся не потому, что он реально нужен, а только потому, что "так научили" и программист просто не знает, как можно это сделать по-другому.

Тут можно было бы еще наковырять по мелочам, но, думаю, того, что сказано пока достаточно. В качестве иллюстрации сказанного можно предложить вот такой пример реализации односвязного списка на C++:

http://coliru.stacked-crooked.com/a/33377b09832a0bd3

  UniDirList, C++
#include <cassert>
#include <iostream>
#include <memory>
#include <type_traits>

template <typename T>
struct ListNode
{
   T value{};
   std::unique_ptr<ListNode> next;

   template <typename...X>
   ListNode(X&&...x) : value{std::forward<X>(x)...}{}
};

template <typename T>
requires (!std::is_reference_v<T>)
class ListNodeIterator
{
public:
   using value_type = std::remove_const_t<T>;
   using node_type = std::conditional_t<std::is_const_v<T>, const ListNode<value_type>, ListNode<value_type>>;

   ListNodeIterator() = default;
   ListNodeIterator(node_type* node) : m_node(node) {}

   T& operator*() const { assert(m_node); return m_node->value; }
   ListNodeIterator& operator++() { assert(m_node); m_node = m_node->next.get(); return *this; }
   bool operator == (const ListNodeIterator& rhs) const { return m_node == rhs.m_node; }

private:
   node_type* m_node{};
};

template <typename T>
requires std::same_as<T, std::decay_t<T>>
class UniDirList
{
public:
   using iterator = ListNodeIterator<T>;
   using const_iterator = ListNodeIterator<const T>;

   UniDirList() = default;
   UniDirList(std::initializer_list<T> values) { for(auto&& value : values) emplace_back(value); }

   bool empty() const { return !m_first; }

   iterator begin() { return iterator{ m_first.get() }; }
   iterator end() { return {}; }

   const_iterator begin() const { return const_iterator{ m_first.get() }; }
   const_iterator end() const { return {}; }

   template <typename...X>
   T& emplace_back(X&&...x) {
      if (m_last) {
         m_last->next = std::make_unique<Node>(std::forward<X>(x)...);
         m_last = m_last->next.get();
      }
      else {
         m_first = std::make_unique<Node>(std::forward<X>(x)...);
         m_last = m_first.get();
      }
      return m_last->value;
   }

private:
   using Node = ListNode<T>;
   std::unique_ptr<Node> m_first;
   Node* m_last{};
};

int main()
{
   UniDirList list = {3.14, 2.71, 1.61};

   list.emplace_back(42.);

   for (double value : list)
      std::cout << value << " ";
}


Эта реализация очень груба и местами даже наивна. И вообще она не нужна, поскольку в стандартной библиотеке есть тот же std::list и другие контейнеры. Тем не менее, даже такая реализация демонстрирует возможности и преимущества C++ по все критериям — и по простоте и наглядности кода, и по обобщенности.
Re: Не могу понять ссылки в C++
Здравствуйте, Worminator X, Вы писали:

WX>Несколько раз пытался освоить C++, и всегда забрасывал это дело — сложный, совершенно непонятный язык.


WX>Вот, допустим, совершенно тривиальный код обхода списка целых чисел на чистом Си с указателями:


WX>Можно переписать его в стиле "Си с классами", здесь пока тоже все очевидно, просто теперь new/delete вместо malloc/free и class вместо struct:


WX>Примерно так я представлял себе C++ в школе и универе, не зная ни про ссылки, ни про операторы, ни про шаблоны и имея смутное представление о множественном наследовании.

WX>Т.к. Pure C, Turbo Pascal и Delphi (и чуть позже Java с C#) решали все необходимые задачи, изучать C++ тогда не было никакой необходимости.

WX>По правилам хорошего тона в C++ нужно всегда использовать ссылки вместо указателей (кроме интеграции с кодом на Си) и по возможности не управлять памятью вручную, т.к. это чревато утечками.

WX>Я попытался переписать код, и вышло примерно следующее (для упрощения пока без шаблонов):
WX>Помимо того, что этот талмуд совершенно непонятен, оно еще и не работает.
WX>Рекурсивный обход списка в recursivePrintIntegerList вроде выводит все как положено, но в foreachPrintIntegerList цикл почему-то выводит одни единицы и не хочет переходить на следующий элемент.
WX>Что нужно исправить? И что можно почитать по теме?
WX>Из учебника Столярова так и не понял, что такое есть ссылка в C++ (вроде синтаксический сахар над указателем, но как-то странно и непонятно работает).
WX>И еще, почему, если в EmptyList сделать пустой конструктор, то на строки const EmptyList empty(); const IntegerList n3(3, empty); компилятор ругается? Вариант с const EmptyList empty(void) не помог.

Я бы начал с того, что нет в С++ такого правила, которое запрещало бы использование сырых указателей. Плохо, когда владение объектом осуществляется через сырой указатель — это действительно чревато и утечками памяти, и риском влететь на неопределенное поведение в случае повторного удаления объекта. Также плохо, когда указатель используется в качестве ссылки на обязательный объект, который не может отсутсвовать. Вообще плохо, когда сырые указатели используются бездумно, просто потому, что программист не видит особой разницы между ссылкой и указателем. В то же время есть случаи, когда использование сырых указателей вполне оправдано — например, в качестве итераторов последовательностей, занимающих непрерывные области в памяти.

Идем далее. Современный С++ заточен, в первую очередь, под решение практических задач. На выбранном примере трудно увидеть преимущества С++ как раз ввиду того, что практическая ценность данного примера стремится к нулю. Суди сам: твой список заточен под конкретный тип данных и у тебя (в рамках данного примера) даже не предусмотрено даже сколько-нибудь общего способа наполнить список произвольными данными. Как только ты захочешь исправить этот недостаток, твой код тут же начнет раздуваться и обрастать костылями, подобными print_integer_list и new_integer_list. Короче говоря, твой оригинальный пример на С лишь создает иллюзию простоты в виду своей полной практической бесполезности.

Идем дальше, твой пример на C иллюстрирует следующую большую проблему — необходимость ручного управления ресурсами. Программист, разрабатывавший класс списка тупо переложил ответственность за освобождение ресурсов на пользователя. Теперь остается только молиться на то, что пользователь вызовет free_integer_list в нужное время, в нужном месте и сделает это ровно один раз. Когда вся программа строится по такому принципу, вот тогда и получаются традиционные для С проблемы: утечки памяти, крэши и неопределенное поведение.

Что еще хочется отметить — абсолютно необоснованное использование полиморфизма времени выполнения — очень распространненная болячка. Мне доводилось видеть много примеров, когда ран-тайм полиморфизм применялся не потому, что он реально нужен, а только потому, что "так научили" и программист просто не знает, как можно это сделать по-другому.

Ну и в качестве вишенки на торте, распростаненный стереотип — "я хотел сделать проще, поэтому без шаблонов". По факту же все наоборот.

Тут можно было бы еще наковырять по мелочам, но, думаю, того, что сказано пока достаточно. В качестве иллюстрации сказанного можно предложить вот такой пример реализации односвязного списка на C++:

http://coliru.stacked-crooked.com/a/33377b09832a0bd3

  UniDirList, C++
#include <cassert>
#include <iostream>
#include <memory>
#include <type_traits>

template <typename T>
struct ListNode
{
   T value{};
   std::unique_ptr<ListNode> next;

   template <typename...X>
   ListNode(X&&...x) : value{std::forward<X>(x)...}{}
};

template <typename T>
requires (!std::is_reference_v<T>)
class ListNodeIterator
{
public:
   using value_type = std::remove_const_t<T>;
   using node_type = std::conditional_t<std::is_const_v<T>, const ListNode<value_type>, ListNode<value_type>>;

   ListNodeIterator() = default;
   ListNodeIterator(node_type* node) : m_node(node) {}

   T& operator*() const { assert(m_node); return m_node->value; }
   ListNodeIterator& operator++() { assert(m_node); m_node = m_node->next.get(); return *this; }
   bool operator == (const ListNodeIterator& rhs) const { return m_node == rhs.m_node; }

private:
   node_type* m_node{};
};

template <typename T>
requires std::same_as<T, std::decay_t<T>>
class UniDirList
{
public:
   using iterator = ListNodeIterator<T>;
   using const_iterator = ListNodeIterator<const T>;

   UniDirList() = default;
   UniDirList(std::initializer_list<T> values) { for(auto&& value : values) emplace_back(value); }

   bool empty() const { return !m_first; }

   iterator begin() { return iterator{ m_first.get() }; }
   iterator end() { return {}; }

   const_iterator begin() const { return const_iterator{ m_first.get() }; }
   const_iterator end() const { return {}; }

   template <typename...X>
   T& emplace_back(X&&...x) {
      if (m_last) {
         m_last->next = std::make_unique<Node>(std::forward<X>(x)...);
         m_last = m_last->next.get();
      }
      else {
         m_first = std::make_unique<Node>(std::forward<X>(x)...);
         m_last = m_first.get();
      }
      return m_last->value;
   }

private:
   using Node = ListNode<T>;
   std::unique_ptr<Node> m_first;
   Node* m_last{};
};

int main()
{
   UniDirList list = {3.14, 2.71, 1.61};

   list.emplace_back(42.);

   for (double value : list)
      std::cout << value << " ";
}


Эта реализация очень груба и местами даже наивна. И вообще она не нужна, поскольку в стандартной библиотеке есть тот же std::list и другие контейнеры. Тем не менее, даже такая реализация демонстрирует возможности и преимущества C++ по все критериям — и по простоте и наглядности кода, и по обобщенности.

Для наглядности можно посмотреть на использование:

сравни это:

int main()
{
   UniDirList list = {3.14, 2.71, 1.61};

   list.emplace_back(42.);

   for (double value : list)
      std::cout << value << " ";
}


и это:

int main(int argc, char *argv[]) {
    struct INTEGER_LIST *numbers;
    numbers = new_integer_list(1, 3);
    if (!numbers) return 1;
    print_integer_list(numbers);
    free_integer_list(numbers);
    return 0;
}


Какой вариант проще для восприятия? Прими также во внимание, что я могу использовать любые типы данных: числа, строки, стандартные и пользовательские классы. И что пришлось бы сделать тебе, чтобы добиться сопоставимой функциональности.