Re: Не могу понять ссылки в C++
От: rg45 СССР  
Дата: 15.06.24 10:00
Оценка: 6 (1) +2
Здравствуйте, Worminator X, Вы писали:

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

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

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

Идем далее. Современный С++ заточен, в первую очередь, под решение практических задач. На выбранном примере трудно увидеть преимущества С++ как раз ввиду того, что практическая ценность данного примера стремится к нулю. Суди сам: твой список заточен под конкретный тип данных и у тебя (в рамках данного примера) даже не предусмотрено даже сколько-нибудь общего способа наполнить список произвольными данными. Как только ты захочешь исправить этот недостаток, твой код тут же начнет раздуваться и обрастать костылями, подобными 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()
{
   for (double value : UniDirList{3.14, 2.71, 1.61})
   {
      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;
}


Какой вариант проще для восприятия? Прими также во внимание, что я прямо в имеющейся реализации могу использовать произвольные типы данных: числа, строки, стандартные и пользовательские классы. И что пришлось бы сделать тебе, чтобы добиться сопоставимой функциональности.
--
Отредактировано 15.06.2024 11:28 rg45 . Предыдущая версия . Еще …
Отредактировано 15.06.2024 11:27 rg45 . Предыдущая версия .
Отредактировано 15.06.2024 10:39 rg45 . Предыдущая версия .
Отредактировано 15.06.2024 10:25 rg45 . Предыдущая версия .
Отредактировано 15.06.2024 10:23 rg45 . Предыдущая версия .
Отредактировано 15.06.2024 10:22 rg45 . Предыдущая версия .
Отредактировано 15.06.2024 10:19 rg45 . Предыдущая версия .
Отредактировано 15.06.2024 10:08 rg45 . Предыдущая версия .
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.