Здравствуйте, 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;
}
Какой вариант проще для восприятия? Прими также во внимание, что я прямо в имеющейся реализации могу использовать произвольные типы данных: числа, строки, стандартные и пользовательские классы. И что пришлось бы сделать тебе, чтобы добиться сопоставимой функциональности.