Для получения триального ключа
заполните форму ниже
Team license
Enterprise license
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Бесплатная лицензия PVS-Studio для специалистов Microsoft MVP
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Для получения лицензии для вашего открытого
проекта заполните, пожалуйста, эту форму
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Мне интересно попробовать плагин на:
** Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

Holy C++

23 Ноя 2022
Автор:

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

Мы опубликовали и перевели эту статью с разрешения правообладателя. Автор статьи — Kelbon (kelbonage@gmail.com). Оригинал опубликован на сайте Habr.

Эта статья холиварная, но зато заставляет задуматься над разными вещами. Прошу обратить внимание, что эта статья стороннего автора. Мнение команды PVS-Studio может отличаться от мнения автора по разным вопросам.

Чтобы не пропустить интересные публикации нашей команды или гостевые посты, приглашаем подписаться на ежемесячную рассылку.

1010_Holy_Cpp_ru/image1.png

union

В первую очередь хочется убрать из языка то, что приводит к частым ошибкам и мешает развитию языка, тут идеальным кандидатом можно назвать union — сумму типов из 70х. В С идея хранения одного типа из нескольких в одном участке памяти выглядит неплохо и сейчас, ведь там все типы — это набор байт с заданным размером.

В С++ же использование union — это автоматическое undefined behavior, например:

#include <string>
union A { int x; float y;};
union B {
  B() {} // требуется написать какой то конструктор и деструктор
  // но обратите внимание, что написать деструктор правильно невозможно
  // (попробуйте, если не верите)
  ~B() {}
  std::string s;
  int x;
};
int main() {
  A value;
  value.x = 5;
  value.y; // undefined behavior, обращение к неактивному члену union
  B value2;
  value2.s = "hello world";
  // undefined behavior, поле s неактивно и используется 
  // (в операторе= для std::string)
}

Как вы видите, использовать union без ошибок просто невозможно. При этом вам постоянно придётся вручную вызывать правильный деструктор для объекта и вместо приравнивания делать placement new в нужное поле. Так зачем же так мучаться, если можно сделать нормальный тип с хорошим интерфейсом БЕЗ какого-либо оверхеда относительно юниона?

Следующий код полностью заменяет юнион, не имеет никакого оверхеда относительно него и имеет более понятный пользователю интерфейс (emplace / destroy).

template<typename ... Ts>
struct union_t {
  alignas(std::ranges::max({alignof(Ts)...})
  std::byte data[std::ranges::max({sizeof(Ts)...});
  
  template<one_of<Ts...> U>
  constexpr U& emplace(auto&&... args) {
    return std::launder(new(data) U{std::forward<decltype(args)>(args)....});
  }
  template<one_of<Ts...> U>
  constexpr void destroy_as() const {
    reintepret_cast<const U*>(reinterpret_cast<void*>(data))->~U();
  }
};

При этом в стандарте просто колоссальное количество исключений для union, бесполезных правил и ограничений. А можно просто взять и забыть про этот отголосок С, в 2022-то году...

1010_Holy_Cpp_ru/image2.png

Массивы

Это может звучать странно, но мы правда можем убрать из С++ массивы, не потеряв ничего и убрав этот чудовищный синтаксис char(&&...arr)[N] (угадайте в комментариях, что это значит).

К тому же массивы почему-то не копируемы и не умеют в мув семантику, что делает их самыми неполноценными типами во всём языке.

Как же их заменить? Рекусивным (или через множественное наследование) туплом с элементами одного типа (да, это было очевидно).

Интересный факт: в тексте стандарта С++ есть исключение аж в цикле for для сишных массивов... Что подтверждает очевидное — массивы безумно плохо соотносятся с остальным языком.

template<typename T, size_t I>
struct array_value { T value; };
template<typename, typename>
struct array_impl;
template<typename T, size_t... Is>
struct array_impl<T, std::index_sequence<Is...>> : array_value<T, Is>...{};

template<typename T, size_t N>
struct array_ : array_impl<T, std::make_index_sequence<N>> {
  // тут какой-то интерфейс массива по вашему желанию
    T& operator[](size_t n) {
        // такая реализация для краткости
        return *(reinterpret_cast<T*>(reinterpret_cast<void*>(this)) + n);
    }
};

Тип void

void по большей части служит для того, чтобы делать под него исключения в обобщённом коде. Было бы гораздо удобнее иметь тип с единственным ничего не значащим значением... Как же сделать такой тип....

struct [[maybe_unused]] nulltype {};
 // Вот и всё... Да и аттрибут [[maybe_unused]] тут разве что для красоты

Все фундаментальные типы...

Кажется мы идём по нарастающей, на что же автор статьи тут замахнулся? На int?!

Да, не удались в С фундаментальные типы, а С++ их унаследовал. Кто в здравом уме будет использовать int, который может занимать 8 байт, но гарантирует свои значения только до 2 ^ 16??? Это буквально создатель ошибок (особенно у новичков).

Заменить это всё можно одним фундаментальным типом byte и указателями. Действительно: с помощью byte и системы типов С++ можно создавать любые типы, в том числе аналогичные int, double, float, bool и т.д. из фундаментального набора.

Тут мы убиваем сразу несколько зайцев — нет больше исключений для фундаментальных типов в разрешении перегрузки, нет исключений в шаблонном коде для наследования (от фундаментальных типов нельзя наследоваться), ну и другие более мелкие исключения для подобных типов уходят в прошлое.

Приведения типов из С

Это. Просто. Не. Должно. Компилироваться. (но оно компилируется) https://godbolt.org/z/fz6eMEeqG

int main() {
    (void)(5), (void)5, void(5);
}

Runtime variadic arguments

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

И даже несмотря на то, что так реализован знаменитый printf(const char* pattern, ... ) (кто не понял, многоточие — это рантайм аргументы! Любые!), это выглядит самый большой костыль в истории программирования, а как этим пользоваться... Ух... Макросы __VA_START__, __VA_COPY__ и громадная куча ещё всего связанного с этим будут сниться в кошмарах сишникам десятилетиями, а С++ пожалуй должен просто удалить этого демона из языка и забыть (и добавить за счёт удаления этого новые возможности пакам шаблонных аргументов).

typedef

Ну тут всё просто, в С++ есть отличная замена этому слову. Просто сравните:

// foo теперь алиас на void(*)(int) (указатель на функцию)
typedef void(*foo)(int);
// то же самое, но на С++
using foo = void(*)(int);

Смысла оставлять typedef в языке нет...))

Функциональные макросы

Это те самые макросы из С, которые принимают аргументы. Именно их обычно называют основной причиной сложности понимания кода. Плохого кода, ведь в современных плюсах использование подобных макросов неприемлемо.

Вот, кажется на этом этапе мы вычистили почти весь С из С++ и почти получили чистые ++ (плюсы). Пора рассмотреть, что стоит удалить тут!

Операторы new и delete

Действительно, зачем нужны в языке эти операторы, если всё давно перенесено на уровень абстракций аллокаторов, а память на низком уровне можно продолжать выделять через malloc?! Как вообще можно было догадаться внести систему (системный аллокатор памяти) на уровень языка?

Вы только посмотрите на даже не правила, а просто список перегрузок одного только new:

1010_Holy_Cpp_ru/image3.png

Нужно только оставить размещающую версию оператора new для вызова конструктора по нужному адресу. Всё остальное, особенно перегрузки new / delete, использовать в современном С++ просто запрещено, если вы не хотите, чтобы вас засмеяли.

Ключевое слово class

Ну тут я просто оставлю ссылку на мою же статью про бесполезность этого ключевого слова: https://habr.com/ru/post/662351/.

Ключевое слово final (запрет наследоваться от типа)

Не имеет ни одного известного мне полезного применения, ломает обобщённый код, вердикт — удалить.

Виртуальные методы

Вызывают громадную кучу ошибок.

Неэффективны, стимулируют писать архитектурно плохие решения, неэффективно использовать память, не позволяют использовать весь остальной язык, если используется ключевое слово virtual. САМОЕ ГЛАВНОЕ — могут быть полностью заменены на другие языковые возможности без потери функционала и с приобретением производительности, удобства, повторяемости кода, проверок на компиляции и т.д.

Реализация динамического полиморфизма без виртуальных функций и их проблем: https://github.com/kelbon/AnyAny.

Методы (указатель на текущий объект внутри реализации типа)

В С++23 появляется (наконец) deducing this, благодаря которому можно будет явно декларировать передачу this в методы типа. При этом такой "метод" будет фактически функцией с точки зрения языка, а значит в последующем (вместе с удалением виртуальных методов) можно будет избавиться от самого понятия МЕТОД в языке С++ и указателя на эту вещь (не дай боже вам перед сном увидеть декларацию указателя на метод).

struct A {
void foo(this A& self);
};

При этом возможно, что постепенно и ключевое слово this потеряет прежнее значение и останется только такое — декларация явной передачи ссылки/значения типа в функцию

Ну вот и всё, помечтали о великолепном holy C++, можете теперь пойти и опять продолжить писать хрень с виртуальным наследованием, забытым виртуальным деструктором на полиморфном типе и сишными кастами, удачи...

Популярные статьи по теме
Продление жизни временных значений в С++: рецепты и подводные камни

Дата: 01 Ноя 2022

Автор: Гость

Прочитав эту статью, вы узнаете следующее: способы, которыми можно продлить время жизни временного объекта в С++; рекомендации и подводные камни этого механизма, с которыми может столкнуться С++ прог…
Как мы баг в PVS-Studio искали или 278 Гигабайтов логов

Дата: 28 Окт 2022

Автор: Григорий Семенчев, Сергей Ларин, Филипп Хандельянц

Предлагаем вашему вниманию интересную историю о поиске бага внутри анализатора PVS-Studio. Да, мы тоже допускаем ошибки, но мы готовы засучить рукава и залезть в самую глубину "кроличьей норы".
0, 1, 2, Фредди забрал Blender

Дата: 26 Окт 2022

Автор: Андрей Карпов

Эта статья могла бы получить название "Как PVS-Studio защищает от поспешных правок кода, пример N7". Однако так именовать статьи становится скучновато. Поэтому сейчас вы узнаете, причём здесь Фредди …
Примеры ошибок, которые может обнаружить PVS-Studio в коде LLVM 15.0

Дата: 25 Окт 2022

Автор: Андрей Карпов

Компиляторы развиваются и выдают всё больше предупреждений. Остаются ли преимущества от использования статических анализаторов кода, таких как PVS-Studio? Да, так как анализаторы тоже развиваются. Пе…
Как PVS-Studio защищает от поспешных правок кода, пример N6

Дата: 20 Окт 2022

Автор: Владислав Столяров

Программисты часто допускают ошибки в коде по невнимательности или из-за спешки. Если вам интересно, как можно быстро находить такие ошибки, то мы рады приветствовать вас в очередной статье из цикла …

Комментарии (0)

Следующие комментарии
Unicorn with delicious cookie
Мы используем куки, чтобы пользоваться сайтом было удобно.
Хорошо