Комментарии 114
Ещё из недостатков: низкая скорость компиляции и высокие требования к железу (какой-нибудь хромиум, боюсь, на среднем ноуте невозможно собрать в принципе).
На виртуалке за ночь собирается. На реальном эире эмке - часа за четыре с паралелльным просмотром фильмов. Но это без суровых оптимизаций, да.
Это вы еще раст не собирали))
Я какое-то вермя работал в одном кабинете с разработчиками аппаратуры на VHDL и Verilog. Они снисходительно смеялись когда я жаловался на скорость компиляции плюсов.
Низкая скорость компиляции присуща всем компилируемым языкам, потому что значительную долю времени компилятор тратит на оптимизацию кода.
Не согласен на счёт всех. Как минимум, есть pascal/delphi и golang, которые компилятся очень быстро. А компиляция крупных проектов на плюсах может занимать длительное время даже с выключенной оптимизацией. А с учётом тьюринг-полных шаблонов она вообще может длиться вечно. :)
А есть ли там LTO, whole program optimization? Мне кажется, что нет.
Например, скорость компиляции MSVC с оптимизациями может быть на порядок, а то и два дольше.
На счёт паскаля не знаю, в го, вроде, нет LTO. Но опять же, плюсы и без оптимизаций дольше компилятся, чем го с оптимизациями. Даже с precompiled headers. Так или иначе, долгая компиляция си++ это недостаток, достойный упоминания.
На моей работе есть крупный плюсовый проект, для сборки которого отдельные девелоперские сервера юзаются, т.к. даже на мощных ноутах это занимает неприлично много времени. Но даже на этих серверах, после пары правок в код приходится ждать минуту-две, чтобы собрать проект. Вроде бы это немного, но достаточно, чтобы отвлечься на браузер и потом обратно вспоминать, что я там проверить хотел.)
P.S. И не дай бог поменять бранч… Иначе проект будет пересобираться заново, что займёт уже не 1-2 минуты, а все 20.
Ноут за 30к, купленный в 2018 году - полная сборка хромиума летом 2021 заняла почти сутки. 23 часа с копейками :D
Не знаю, следует ли это записывать в недостатки плюсов (в любом случае, объем кода просто монструозный, на любом языке, кмк, сборка длилась бы десятки часов, не знаю), просто делюсь фан фактом из жизни :D
Ну то, что это заняло меньше суток, это весьма обнадёживающе.))
Я ради интереса собирал kubernetes на raspberry pi 4 с 4гб памяти — чистая сборка занимает 25 минут. Наверняка у него кодовая база гораздо меньше. Допустим, судя по быстрому гуглению оценок cloc, в 25 раз меньше. В этом случае, сборка бы сопоставимого с хромиумом проекта на го заняла бы на малинке 10 часов. :)
Ни в одном другом инструментарии нет таких чудовищных проблем со сборкой проекта, как в тулчейне C++. К каждому проекту всегда прикладывают коротенькую инструкцию, странички этак на четыре, с описанием того, как всё это собирать. А если мы хотим из-под Linux собрать что-то под винду, то сложность сборки вообще устремляется в небеса. Для сборки ряда крупных проектов помимо собственно C++ вам потребуется доустановить Node.js, Perl, Python и Go, потому что процесс сборки использует скрипты на нескольких разных языках одновременно.
Отдельно доставляет крайне сложная и запутанная система ключей компилятора и линкера, которые взаимодействуют весьма непредсказуемым образом, а их подбор для успешной сборки чужого проекта — отдельный вид шаманства. Не удивлюсь, если окажется, что система ключей там уже давно Тьюринг-полная.
Ко всему еще можно добавить проблемы с версиями компилеров, когда пытаешься собрать зависимости и выясняется что gcc 11 уже собрать не может, будь добр откатиться чтобы собрать. И начинаются танцы в контейнерах. А зависимостей то уже может быть столько, что на пальцах не пересчитать.
К счастью, современные пакетные менеджеры(более-менее уверенно могу говорить только о vcpkg и conan, но они не единственные) весьма успешно решают проблемы с прикручиванием к своему проекту библиотек.
Если кто-то уже завернул бибиотеки в пакет, конечно. В противном случае можно подумать, надо ли оно, и либо завернуть самостоятельно и даже опубликовать, либо подумать о переезде на что-то более широко распространенное.
Сборка же из исходников рандомных проектов, в которых могут быть задействованы "нетрадиционные" возможности системы сборки или утилиты, действительно остаётся болью.
Еще недостаток - трудность и неудобство отладки.
Выход - не позволять слишком разрастаться С++ проекту. Отвечать только за малую часть проекта. Делать так, чтобы части проекта были бы по возможности независимы. А вообще, всем угодить нельзя!
Какое же превратное понимание у опрашиваемого того, чем должен и чем не должен являться язык программирования.
Менеджера пакетов нет — ах какая катастрофа!!! Ещё через 10 лет программисты, похоже, будут гнобить тот или иной ЯП только за то, что к нему в комплекте не поставляется длинноногая секретарша.
Забавно, что почти все подчёркнутые им плюсы я считаю минусами, а названным им минусам наоборот рад. Да, когда-то давно в первые пару лет использования C/C++ я тоже злился от разделения на cpp- и h-файлы. Но затем понял фишку, постиг дзен и поражался, какой же я был наивный дурак я был (чисто в духе парадокса Даннинга-Крюгера). А здесь человек 15 лет (!!!) пишет на C++, но видимо до сих пор не вдупляет в то, зачем нужны прототипы, в чем разница между declaration и definition, и почему заголовочные файлы это благо.
Если не трудно, поясните, пожалуйста, свою точку зрения, а то я хоть и не часто на плюсах пишу, но с автором согласен в целом, и что такого хорошего дают заголовочные файлы - не могу представить.
Тем более заголовочные файлы — это не только оглавление. Было бы так — их давно бы генерила специальная тулза прямо из cpp.
Когда язык создавался — это было оправданно и, даже, прогрессивно (в том же Паскале с модулями — этот заголовок надо было писать в том же файле, что и код, всё так же повторяясь).
Но теперь это — атавизм, от которого надо было-бы начинать избавляться ещё лет десять назад (например, сделать его необязательным для простых случаев) — как раз сейчас бы уже было бы намного лучше.
Но теперь это — атавизм
Что поменялось «теперь» по сравнению с временами, когда язык создавался? Люди стали ходить на голове? Число Пи достигло значения 8? Поменялась размерность пространства-времени?
Или просто люди, подержавшие в руках инструмент, допускающий чрезвычайно широкий и гибкий workflow использования, но использовавшие его только стереотипным и самым простым образом,обнаглели, и решили, что раз им что-то не пригодилось, то и никому это не нужно?
Заголовочные файлы появились не в C++, а в C, тогда как C++ просто унаследовал этот функционал из-за совместимости с C.
Что поменялось за это время: выросла кодовая база, число зависимостей. Пока программы состояли из пары десятков файлов и умещались в 640 кб памяти, всё было в порядке. Сейчас программы огромные и сложность компиляции из-за парсинга кучи вырастает экспонециально.
Так появились интерфейс и его реализация, которая может меняться не изменяя интерфейса. И интерфейс реализации складывают в заголовки, а реализацию по c/cc/cxx/c++/cpp/… файлам (в идеале).
Но можно и микроскопом забивать гвозди — вот именно так и появились header only c++ библиотеки.
Ага, только вот шаблоны ломают эту логику. Реализация шаблонов возможна только в заголовочных файлах. Это привело к тому, что при каждой компиляции cpp-файла компилятор каждый раз проходится по куче инклюдов и парсит их заново. А потом линкер выбрасывает одинаковые реализации шаблонов.
Попробуйте писать на C# и увидите, насколько просто и гибче скрываются детали реализации )
ps: Если хочется новых ощущений надо пробывать erlang
С F# не работал, но предполагаю, что у него своя нишша. И вопрос не в том, что в C# все гибко, а в том, что в нем ты просто делаешь, что тебе надо, без раздумий о том, какие костыли на этот раз запилить, чтобы оно хоть немного работало. А не как в C++: атрибуты и прочая метаинформация? ПФ... Да кому это нужно! Рефлексия? На чайнике рефлексия не нужна, а значит она не нужна вовсе! Серализация? Да без проблем! Сейчас расчехлим пару сотню костылей, которые уже через неделю все забудут как поддерживать и все ) Сеть? А что, ей кто то пользуется? Придётся поискать нужные библиотеки и ещё несколько часов (если не дней) мудохаться с их подключением!
Про магию с шаблонами C++, которую трудно понять новичкам (а может и не только им) и которая в случае ошибок выдаёт черти что, наверное даже и заикаться не стоит, да? )
Это один из тех случаев, когда приходится отстаивать и защищать настолько очевидную и фундаментальную вещь, что даже теряешься и не можешь сходу выбрать линию аргументации.
Грубо говоря, когда в обществе долгое время существовал консенсус, что убивать не хорошо, что справедливость это благо, а несправедливость это плохо, и вдруг приходит молодое поколение и внезапно ставит вопрос: «а почему собственно убивать не хорошо?».
Ладно, попробую. Заголовочные файлы это вообще явление из зоны ответственности препроцессора. Никто вас не заставляет их использовать. Можете не пользоваться ими, а объявления всех типов и прототипы всех функций объявлять прямо в своем cpp-файле. Можете вообще всю свою программу, пусть в ней и 200 тысяч строк кода, уместить в одном единственном cpp-файле.
Но если вы разумный программист, вы скорее подробите свой проект на множество cpp-файлов, потому что будете осознавать преимущества такого разделения. Которое, к слову, заключается не только в том, что в маленьких файлах легче ориентироваться, как многие думают, а главным образом в том, что каждый cpp-файл можно компилировать отдельно и в отрыве от всех остальных, а значит, если мы подправили реализацию всего одной функции (из, допустим, десяти тысяч), можно перекомпилировать только этот cpp-файл, а не весь проект целиком, а потом слинковать из старых объектных файлов и одного нового итоговый исполняемый файл, что гораздо быстрее.
Так вот, если у вас 190 штук cpp-файлов, и во всех вы используете структуры VECTOR3D, WAVEHDR, ANOTHERSTRUCT, то в каждом cpp-файле придется иметь объявление этих структур. 190 × 3 объявлений.
И вот здесь, чтобы не плодить 190 копий объявления трёх структур, чтобы уйти от WET-антипаттерна и использовать DRY-принцип, на сцену выходит препроцессорая фича #include, позволяющая повторяющиеся вещи вынести в один файл и избавиться от повторений, за исключением того, что сама директива #include и имя файла, указанное в ней, таки будет повторяться много раз.
То есть это прямо киллер-фича, которую сделали хоть и не только для, но в значительной степени именно для элиминации повторяющегося исходного кода. Что крайне важно не только потому, что не надо тратить время и силы на написание/контраст одинаковых кусков кода, но и позволяет иметь одно место с авторитетным описанием сущности (как гласит принцип DRY), что исключает вероятность при добавлении нового поля в структуру обновить описание её везде, кроме пары мест.
И вот приходит человек и говорит, что h-файлы, которые созданы чтобы помогать бороться с дублированием, заставляют его писать что-то дважды!
И дело-то видимо не в том, что человек против идеи includeинга файлов на стадии препроцессинга или не понимает как эти пользоваться. А дело скорее всего в том, что человек против идеи прототипов функций, а также он испорчен и избалован IDE, которая прячет процесс сборки и представляет его пользователю как нечто непрозрачное, и человек перестает думать отдельно о линковке, отдельно о компиляции каждого отдельного исходного файла независимо от остальных, а начинает считать, что есть некий черный ящик под названием «компилятор C++», который как-то там вызывается один раз сразу на весь проект, видит сразу весь проект, обрабатывает весь проект и выплёвывает готовый исполняемый файл, а раз так, ну уж наверное он может в одном cpp-файле увидеть определение структуры или функции, а встретив упоминание чего-то такого в другом cpp-файле, который ни сном ни духом про первый файл, самостоятельно догадаться, что вот именно из того файла сущности здесь и упоминаются.
Для примера, тот же C#, так же позволяет писать код и разделять его на разные файлы, но не заставляет тебя самостоятельно писать заголовки. Компилятор сам генерирует все необходимые метаданные. Генерация кода автоматически как раз соответствует принципу DRY.
Понятно. Это что-то вроде желания, будь вы из сферы машиностроения, забыть ненавистное вам оформление проектной документации, а сразу вы тачивать детали «из головы», но чтобы потом по эти деталям проектная документация (чертежи, 3D-модели, спецификации) сами генерировались.
При нормальном подходе люди сначала проектируют, потом за изготовление берутся, и в программировании я обычно начинал с мыслительной работы по проектированию будущего программного продукта, и первое, что появлялось из ничего при зачинании новой программы или нового компонента в программе, это были всегда h-файлы. А вам эта стадия не интересна.
Точно так же в других языках программирования есть такие абстракции: interface
в C#, например.
Кстати, с h-файлами есть недостаток: вы не можете объявить только публичную часть класса. Вам придётся в h-файле объявить и все приватные поля, и все приватные методы.
// a.h
#pragma once
struct A {
static A* create();
virtual int fn()=0;
virtual ~A() {}
};
// a.cpp
#include "a.h"
struct A1 : A {
int private_variable;
int fn() { return private_variable; }
};
A* A::create() {
A1 *a=new A1();
a->private_variable=49;
return a;
}
Ну нет, так дело не пойдёт. Вы на пустом месте зачем-то завели виртуальный класс. А слабо в заголочном файле описать именно `A1`
?
В C#, например, так делать можно:
// Interface part
partial class A1
{
public partial int fn();
}
// Implementation part
partial class A1
{
int private_variable;
public partial int fn() { return private_variable; }
}
Вы на пустом месте зачем-то завели виртуальный класс
Так это как раз недостатки C++, в чистом C такой фигни не было:
// a.h
typedef struct A A;
int A_init(A** a);
void A_done(A **a);
int A_fn(A* a);
// a.c
#include <stdlib.h>
struct A {
int private_variable;
};
int A_init(A** res) {
A *a=(A*)malloc(sizeof(A)); if (!a) return 1;
a->private_variable=49;
*res=a;
return 0;
}
int A_fn(A* a) {
return a->private_variable;
}
void A_done(A **a) {
if (*a) { free((void*)*a); *a=0; }
}
Кстати, с h-файлами есть недостаток: вы не можете объявить только публичную часть класса. Вам придётся в h-файле объявить и все приватные поля, и все приватные методы.
Потому что в момент, скажем, создания экземпляра класса на стеке компилятор должен знать его размер. Откуда он будет знать размер, если полного описания класса нет? Создать экземпляр класса of incomplete type не получится. Плюс создавать экземпляр класса компилятор должен уметь даже если у него нет ничего, кроме декларации в h-файле и уже собранной библиотеки с методами класса.
Потому что в момент, скажем, создания экземпляра класса на стеке компилятор должен знать его размер.
А это уже детали реализации. Компилятор вполне может узнать эту информацию из соседних cpp-файлов. Просто в угоду бинарной совместимости с C такую возможность, по всей видимости, сочли нецелесообразной.
Компилятор вполне может узнать эту информацию из соседних cpp-файлов.
Так их вполне может не быть. Разработчик библиотеки свободно может выбрать ее распространение только в бинарном виде (.lib/.a/.dll/.so). А создавать экземпляр класса нужно уметь вне зависимости от способа распространения библиотеки.
Бинарные библиотеки (.a/.dll/.so) нельзя использовать из C++ напрямую, для этого нужен клей в виде .lib-файла.
Ну а так как .lib-файл содержит не только код, но и метаданные, я не вижу никаких проблем, чтобы включать в .lib-файл информацию о размере типа.
Бинарные библиотеки (.a/.dll/.so) нельзя использовать из C++ напрямую, для этого нужен клей в виде .lib-файла.
Что, простите? Заголовочный файл (lib.h
):
#pragma once
#include <string>
class C
{
public:
C();
const std::string &str();
private:
std::string s;
};
Реализация (lib.cpp
):
#include "lib.h"
C::C()
: s("HELLO FROM LIB")
{
}
const std::string &C::str()
{
return s;
}
Вызывающий код (main.cpp
):
#include <iostream>
#include "lib.h"
int main()
{
C c;
std::cout << c.str() << std::endl;
}
Собираем:
$ g++ -c -o lib.o lib.cpp
$ ar rcs lib.a lib.o
$ g++ -o main main.cpp lib.a
Запускаем:
$ ./main
HELLO FROM LIB
Никаких проблем.
P.S. то же самое с динамической библиотекой:
$ g++ -fpic -shared -o lib.so lib.cpp
$ g++ -o main main.cpp lib.so
$ export LD_LIBRARY_PATH=.; ./main
HELLO FROM LIB
Ну а так как .lib-файл содержит не только код, но и метаданные, я не вижу никаких проблем, чтобы включать в .lib-файл информацию о размере типа.
Ну да, компилятор должен теперь шариться не только по .cpp-файлам, но еще и по (видимо, специально для этого введенным) секциям всех подключаемых библиотек в поисках информации о полном размере используемых типов, чтобы избавить вас от необходимости показывать приватные поля в .h-файлах. Вашими бы устами... да мед пить.
Что, простите?
Вы в своём примере подключаете не бинарный файл, а промежуточное представление (.lib). В отличие от .so/.a/.dll, вы не можете сделать LoadLibrary/dlopen и вызвать код оттуда.
P.S. то же самое с динамической библиотекой:
Под виндой, кстати, так не получится, там немного через жопу сделано.
Ну да, компилятор должен теперь шариться не только по .cpp-файлам, но еще и по (видимо, специально для этого введенным) секциям всех подключаемых библиотек в поисках информации о полном размере используемых типов, чтобы избавить вас от необходимости показывать приватные поля в .h-файлах.
Именно так.
Вы в своём примере подключаете не бинарный файл, а промежуточное представление (.lib). В отличие от .so/.a/.dll, вы не можете сделать LoadLibrary/dlopen и вызвать код оттуда.
Я подключаю не "промежуточное представление", с чего вы это взяли? Нет там никакого "промежуточного представления". Я подключаю статическую (.a) и динамическую (.so) библиотеку в том виде, в котором они будут распространяться. Для этого достаточно иметь собственно библиотеку (один файл .a или .so) и заголовочный файл, больше ничего не требуется. Динамическая библиотека у меня загружается автоматически при запуске бинаря, а не вручную через dlopen()
, это так, но это связано не с наличием или отсутствием некоего "промежуточного представления", а с тем, что dlsym()
не совместим с сущностью, называемой в C++ "pointer to member function". Статический метод класса, однако, вызвать таким образом будет вполне возможно. Под виндой, кстати, вполне возможно что удастся вызвать даже и нестатический метод, но я давно не тренировался, так что не уверен на 100%.
Именно так.
Обратитесь в комитет с proposal.
Я подключаю статическую (.a)
Да, перепутал. Тогда это то же самое, что и (.lib) — промежуточная форма.
Я могу эту .a/.lib подключить из C# или Go?
Не знаю, что вы называете "промежуточной формой". .a - это архив из объектных файлов (.o), стандартный формат статической библиотеки в *nix.
Я могу эту .a/.lib подключить из C# или Go?
Это статическая библиотека, используется только компоновщиком (типа ld
). Компоновщик достает из нее нужные объектные файлы и статически прилинковывает к собираемому бинарю, как будто они были только что созданы компилятором - с той разницей, что созданы они были не только что, а давно и, возможно, совсем на другой машине.
Не знаю, что вы называете "промежуточной формой"
Очевидно, то, что нельзя просто взять и запустить.
Объектный модуль (.o) — это не бинарь, это промежуточная форма (compilation unit).
.a - это архив из объектных файлов (.o),
Ну да, архив из промежуточных форм.
Кстати, его конкретное представление стандартом не описывается.
Очевидно, то, что нельзя просто взять и запустить.
Динамическую библиотеку (.so или .dll) тоже нельзя просто взять и запустить, это тоже "промежуточная форма" или нет? Хочу разобраться в терминологии. В любом случае, для подключения .so .a не нужен, нужен только .so и .h.
Кстати, его конкретное представление стандартом не описывается.
Конечно, нет. Он же не зависит от конкретного языка.
Динамическую библиотеку (.so или .dll) тоже нельзя просто взять и запустить, это тоже "промежуточная форма" или нет?
Можно. dlopen
, dlsym
. Отличие .so/.dll от .out/.exe в том, что в последних есть точка входа для прямого запуска.
В любом случае, для подключения .so .a не нужен, нужен только .so и .h.
А под виндой для подключения .dll нужен и .lib, и .h.
Конечно, нет. Он же не зависит от конкретного языка.
Он зависит от принятых соглашений. Например, ничто не мешает создать свои компилятор и линкер со своим представлением .o и .a. Ну не будет оно совместимо с экосистемой gcc/ld, ничего страшного. Просто никто этим не будет пользоваться.
Можно.
dlopen
,dlsym
.
Видимо, у нас разные понятия о "взять и запустить".
А под виндой для подключения .dll нужен и .lib, и .h.
Да, под виндой нужна еще дополнительная прослойка в виде import library. Но это не имеет отношения к C++ как таковому, просто в Windows так принято делать implicit linking с DLL. В системах с ELF это не требуется.
Он зависит от принятых соглашений. Например, ничто не мешает создать свои компилятор и линкер со своим представлением .o и .a. Ну не будет оно совместимо с экосистемой gcc/ld, ничего страшного. Просто никто этим не будет пользоваться.
Под большинством современных *nix .o - это ELF. Такой же, как исполняемые файлы и динамические библиотеки. А то, что "никто не мешает, но никто не будет пользоваться" - это правда. Поэтому у меня и сарказм на тему ваших представлений о том, где C++ компилятор должен находить данные о размере объектов.
Видимо, у нас разные понятия о "взять и запустить".
Да. Взять и запустить код из файла. В случае .lib/.a для этого обязательно нужен определённый компилятор, понимающий формат этого файла. В случае .dll/.so — API операционной системы. Кстати, .out/.exe тоже через вызовы функций надо запускать (CreateProcess, exec).
Но это не имеет отношения к C++ как таковому,
Да, не имеет. Стандарт C++ этого не описывает, поэтому и получается подобный зоопарк.
Поэтому у меня и сарказм на тему ваших представлений о том, где C++ компилятор должен находить данные о размере объектов.
Да, обратная совместимость, которая вставляет палки в колёса. Но ничто не мешало просто расширить метаданные, не поломав совместимость: подцепляем из C — размера типа не видим, подцепляем из C++ — видим.
В случае .lib/.a для этого обязательно нужен определённый компилятор, понимающий формат этого файла.
Как я уже выше сказал, под большинством *nix .a - это архив объектных файлов (.o) стандартного формата (ELF), не зависящего от компилятора. ld
способен собирать из них исполняемый файл без посторонней помощи, ничего не зная о компиляторе, который их сделал. При большом желании их можно даже запустить вручную.
Да, не имеет. Стандарт C++ этого не описывает, поэтому и получается подобный зоопарк.
Так dll'ки же не только C++ может создавать. Почему то, как их создавать, должно быть описано в стандарте? Стандарт ортогонален конкретному формату исполняемых файлов и динамических библиотек на конкретной платформе.
Да, обратная совместимость, которая вставляет палки в колёса.
Дело не в обратной совместимости, а в нежелании завязываться на конкретный формат объектных файлов и библиотек, это сознательно оставлено на откуп конкретной платформе.
Как я уже выше сказал, под большинством *nix .a - это архив объектных файлов (.o) стандартного формата (ELF), не зависящего от компилятора.
Ну да, так сложились звёзды, что компилятор C++ в Unix-подобных ОС генерит файлы стандартного формата.
Почему то, как их создавать, должно быть описано в стандарте?
Стандарт не может описывать, как хранить, но может описывать, что должно храниться в этих файлах.
Дело не в обратной совместимости, а в нежелании завязываться на конкретный формат объектных файлов и библиотек, это сознательно оставлено на откуп конкретной платформе.
Да, в 1983 году сложно было предсказать, какие проблемы с C++ вылезут через много десятилетий.
Стандарт не может описывать, как хранить, но может описывать, что должно храниться в этих файлах.
Ммм, а зачем? Чтобы заведомо вынуждать генерить файлы нестандартного и несовместимого ни с чем формата, с которыми не сможет работать стандартный компоновщик? И все ради того, чтобы .h-файл не прикладывать? И что бы мы выгадали? Вместо .so и .h был бы .so и какой-нибудь .pkg (а еще веселее - И .so И .h И .pkg)? Шило на мыло.
Да, в 1983 году сложно было предсказать, какие проблемы с C++ вылезут через много десятилетий.
Проблемы есть везде, просто везде они разные. Такова жизнь.
И все ради того, чтобы .h-файл не прикладывать?
Нет. Всё ради того, чтобы не выкладывать кишки класса наружу. То есть чтобы в .h-файле были только публичные методы и поля.
Чтобы заведомо вынуждать генерить файлы нестандартного и несовместимого ни с чем формата, с которыми не сможет работать стандартный компоновщик?
Перечитайте, пожалуйста, ветку обсуждений. Мне уже приходится повторять то, о чём я писал ранее. Не заменить формат, а расширить его.
Перечитайте, пожалуйста, ветку обсуждений. Мне уже приходится повторять то, о чём я писал ранее.
Мне тоже приходилось это делать, и не раз. Но я же не жалуюсь.
Не заменить формат, а расширить его.
А откуда вы знаете, что на системе XYZ его вообще можно расширить так, как вам хочется? Это же вполне может оказаться и вовсе невозможно - вы же не знаете, что там за формат.
А теперь представьте, что есть языки, где подключение нового класса не равно подключению файла и где нет разделения на заголовочные файлы и файлы с исходным кодом, для описания интерфейсов используются именно интерфейсы и при этом все компилируется в разы быстрее, чем в C++, а compile-time ошибки всегда указывают на строку, где они произошли, а не на объектный файл )
А главным образом в том, что каждый cpp-файл можно компилировать отдельно и в отрыве от всех остальных
К сожалению, это не только плюс, но и минус. Это ограничивает возможность использования файлов как способа организации структуры проекта и навигации по нему.
Например, скомпилировать один файл на 10000 строк займёт значительно меньше времени, чем 100 файлов по 100 строк. Поэтому приходится тесно связанные вещи пихать в один файл внавал.
если мы подправили реализацию всего одной функции (из, допустим, десяти тысяч), можно перекомпилировать только этот cpp-файл, а не весь проект целиком, а потом слинковать из старых объектных файлов и одного нового итоговый исполняемый файл, что гораздо быстрее.
Сценарий, когда меняется только реализация функции и ничего больше, достаточно редкий. Часто правке подвергается и сам интерфейс. Если мы хотим добавить, удалить, либо поменять сигнатуру функции, перекомпиляции подвергнутся ВСЕ файлы. А, как я написал выше, перекомпилировать один большой файл выйдет намного быстрее, чем много маленьких.
Так вот, если у вас 190 штук cpp-файлов, и во всех вы используете структуры VECTOR3D, WAVEHDR, ANOTHERSTRUCT, то в каждом cpp-файле придется иметь объявление этих структур. 190 × 3 объявлений.
Да, это особенность компиляции программ C/C++: мы компилируем каждый файл в отрыве от остальных, а потом пытаемся собрать воедино из объектников.
Но в других языках такой проблемы нет: достаточно описать структуру в одном из файлов, и тогда она будет автоматически доступна и из других файлов. Компиляторы работают на уровне проектов, а не опускаются до отдельных файлов.
А дело скорее всего в том, что человек против идеи прототипов функций
В идее прототипов функций ничего плохого нет. Плохо то, что классы и шаблоны на эту идею плохо ложатся.
Написал не менее длинный ответ на этот коммент, и в последний момент он канул в небытие. Слава новому WYSIWYG-редактору Хабра!
Второй раз набирать нет желания: я вообще-то с телефона набирал, так как лежу с температурой 39, палец и так отваливается. Может быть потом
Например, скомпилировать один файл на 10000 строк займёт значительно меньше времени, чем 100 файлов по 100 строк. Поэтому приходится тесно связанные вещи пихать в один файл внавал.main.cpp:
#include "1.cpp"
#include "2.cpp"
....
?И вот здесь, чтобы не плодить 190 копий объявления трёх структур, чтобы уйти от WET-антипаттерна и использовать DRY-принцип, на сцену выходит препроцессорая фича #include, позволяющая повторяющиеся вещи вынести в один файл и избавиться от повторений, за исключением того, что сама директива #include и имя файла, указанное в ней, таки будет повторяться много раз.
То есть это прямо киллер-фича, которую сделали хоть и не только для, но в значительной степени именно для элиминации повторяющегося исходного кода.
Я дико извиняюсь, вы на полном серьезе думаете, что в других языках нет инклуда?
Инклуд мог бы быть киллер-фичей лет 40 назад, но сегодня хочется чего-то больше, чем функции, которая вставляет содержимое другого файла с указанным именем.
Например, как в современных языках: создание неймспейсов, связанных с путем до файла или автоматическая организация модулей в зависимости от структуры директорий, итд итп.
Всего этого в с++ нет, не предвидится в ближайшие несколько лет и можно только надеяться, что работать будет нормально и все этим будут пользоваться.
Но круто, что кого-то в 2022 может восхитить код, собирающий один файл из нескольких.
Например, как в современных языках: создание неймспейсов, связанных с путем до файла или автоматическая организация модулей в зависимости от структуры директорий, итд итп.
М-м-м. Это как в Java/Kotlin, где ты должен сначала трудолюбиво разложить файлы по папочкам, потом в начале каждого файла не забыть трудолюбиво написать package com.horns.and.hooves.api.shit.auth
? Крутяк. А потом, если Horns & Hooves потребуется переименовать в Hooves & Horns, процесс надо будет повторить - ну или понадеяться на какую-нибудь киллер фичу очередной мега IDE по решению проблемы, которой бы не было, если бы в язык не была добавлена столь по-дурацки спроектированная "особенность". Запах современного языка настолько силен, что глаза вытекают.
Всего этого в с++ нет, не предвидится в ближайшие несколько лет и можно только надеяться
что в C++ такого не будет никогда.
Даже если и ничем (хотя, конечно, есть чем - после перемещения в другой каталог хедеры обычно править не требуется) - в чем "модерновость"-то тогда? :)
Ну вообще-то требуется — нужно обновить include guard.
Не нужно :)
В отсутствии прпрепроцессора.
Утверждение, что наличие (ну или отсутствие) препроцессора как-то доказывает отсталость (или модерновость), несколько голословное на мой вкус :) Отдаёт эдакой вкусовщиной, знаете ли.
Как не нужно, если в гарде так или иначе должен быть закодирован путь к хедеру на случай двух хедеров с одинаковым именем в разных папках?
#pragma once
:) Да, это нестандартно, это имеет свои минусы, но де-факто это есть.
Препроцессор отстал тем, что это тупая подстановка текста, которая вынуждает парсить и оптимизировать все эти вот шаблоны каждый раз, при каждом включении хедера.
Знаете, как говорят - не смотрите на это как на проблему, смотрите, как на возможность :)
И возможность чего это?
Возможность осуществить тупую подстановку текста (и игнорировать часть текста в зависимости от текста, который был тупо подставлен перед этим), конечно же! В Java/Kotlin такое возможно? Нет :)
А зачем это?
Пригодится.
Ими, к счастью, мир не ограничивается.
Хаскель уже можно записывать в отсталые языки с препроцессором, или пока погодить? :)
Нужно использовать `#pragma once`. Иначе противоречие: мы поддерживаем какие-то старые подходы в языке, тем самым потокаем системам которые древнее мамонтов, а потом требуем чего-то современного?
Как раз в C++ в этом случае в каждом C++ файле после того, как препроцессор вставил эти заголовочные файлы внутрь файлов исходного кода будут свои копии этих структур, которые компилятор каждый раз для каждого файла компилит. 190 раз
Тем временем в нормальных языках начиная ну с Turbo Pascal и далее везде эти структуры скомпилятся один раз, и все остальные единицы трансляции будут видеть их в виде бинарных метаданных
Поэтому существуют многочисленные «лагеря несогласных» с «де-факто стандартным» CMake: Make/Ninja/Meson/Build2 и другие. До сих пор нет по-настоящему удобной интеграции с IDE (та же VisualStudio).
Довелось поработать с CMake. Но относительно недавно (чуть более полугода назад) наткнулся на https://premake.github.io/. На мой взгляд очень простая, но в то же время очень мощная система генерации make-файлов. Очень интересно узнать, использует ли кто-то данный инструмент у себя в средних и больших проектах ?
Разница между комментаторами утверждающими что с С++ все хорошо и автором статьи указана в самом начале - он попробовал другие более современные языки. Расширяйте свой кругозор, это полезно. (Особенно смешно читать было про заголовочные файлы которые уменьшают дублирование кода)
Эк вы безапелляционно рассуждаете. Прямо как в известном анекдоте:
— Запомни, сынок, умный человек всегда во всем сомневается. Только дурак может быть полностью уверенным в чем-то.
— Ты уверен в этом, папа?
— Абсолютно, сынок!
(Особенно смешно читать было про заголовочные файлы которые уменьшают дублирование кода)
Человеку, который, очевидно, никогда не делал библиотеку, которая должна одновременно и с соблюдением принципа DRY подключаться, скажем, из C, C++ и Objective C кода, это, может, и смешно - но исключительно из-за недостатка опыта и кругозора. Выше комментатор правильно сказал про людей, обнаглевших от привычки к использованию гибких инструментов исключительно стереотипным образом.
Выше комментатор правильно сказал про людей, обнаглевших от привычки к использованию гибких инструментов исключительно стереотипным образом.
А просветите тогда, как можно модно и молодежно использовать #include ?
Я же написал выше:
Человеку, который, очевидно, никогда не делал библиотеку, которая должна одновременно и с соблюдением принципа DRY подключаться, скажем, из C, C++ и Objective C кода, это, может, и смешно - но исключительно из-за недостатка опыта и кругозора.
Использование #include
не требует от каждого из этих компиляторов уметь парсить код всех остальных для того, чтобы выделить экспортируемые сущности, это раз (это уж не говоря о случае, когда библиотека доступна в бинарном виде). Во-вторых, в заголовочном файле допустима условная компиляция, которая позволяет "спрятать" те интерфейсы, которые компилятор на требуемом языке может не понять, и "показать" те, которые он понять может. Это как минимум. Вот в C++20 добавляют модули, но они не годятся для такого случая, так же, как не годятся и ObjC-специфичные вещи типа@import
. А #include
годится.
Вы таки не поверите, но условная компиляция есть даже там, где никогда этих самых заголовочных файлов не было )
У c++ есть один недостаток - я запарился его учить.
Напоминает древний культ
У цитате Страуструпа:
Garbage is generated by C++ programmers instead.
Обычно, я не читаю книги по программированию - предпочитаю ковыряться в чужом коде.
Но, эта книга стала исключением - "C++ concurrency in action"
Я вернулся (после C#, Kotlin) к C++ и обнаружил, что в "мое отсутствие" в нем всё кардинально изменилось.
Продолжая тему C++03: C++(C++03) был долгое время плохо оптимизирован, не смотря на заявления Bjarne Stroustrup о Zero-overhead principle.
В стандартной библиотеке C++ даже в 2020 не было одной важной сортировки. Даже у Кнута в первых редакциях она была!
Я лично пользуюсь стронними библиотеками т.к. других вариантов C++ мне не даёт.
Проблема cin
и cout
(правдая было или не было автор уже не помнит). Решение
Я тупо переходил на C и Java в таких случаях.
Т.е. то что printf
и scanf
можно использовать из С++ вы не знали, и не знаете до сих пор? :) Про то что С++ позволяет вам напрямую работать с файлами если вам действительно нужен быстрый вывод, я уже молчу. Но вообще msvc известен своей криворукостью.
Приплюснутый, плюсы и «кресты»: за что мы любим и ненавидим C++