In defense of complicated programming languages

https://viralinstruction.com/posts/defense/

1 Like

Bineinteles ca treaba asta e falsa.

2 Likes

Saptamanile astea mi-am bagat nasul prin kernelul de la Linux putin si uite ca se descurca bine mersi si fara clase.

Ba chiar as sari sa zic ca e un cod mult mai citibil decat daca ar fi scris oop in C++. Sa nu mai zic ca o recompilare dureaza penibil de putin.

C-ul nu ar fi un limbaj complex; defapt e un limbaj simplu in ideologia sa. Dar uite ca se fac lucruri complexe cu el, care tipa foarte mult pentru utilitatea lui. Versus, un limbaj complex folosit pentru chestii simple care nu-mi zice nimic despre cat de util este in practica.

1 Like

E foarte discutabila treaba asta.
De obicei depinde foarte mult de experienta. Daca ai experienta mare cu C dar nu cu C++, poate.

1 Like

Au reusit prin niste macro-uri baietii de la kernel sa faca module. Ceva la care C++ inca se chinuie ca nu vad inca pe cineva folosind modulele din C++20 (poate doar Microsoft).

Avand mai multa experienta cu C++ decat C zic ca e mai citibil codul. Acum dezavantajele (cel putin la kernel) este ca e mai greu sa-ti dai seama de dependintele dintre fisiere, unde trebuie sa te bazezi pe o unealta externa numita cscope.

Eu am vazut lipsa claselor la testare, in Java cum ai reflection ai posibilitatea sa te folosești de clase ca să generezi date. Vezi JFixture/AutoFixture.

Modulele din kernel sunt foarte diferite față de ce vor să fie modulele din C++.

Eu o să vin cu un counterpoint, din experiența mea limitată though. Am folosit la muncă C la un proiect la care nu prea aveam altceva de ales în afară de C sau C++ din diverse motive, și am ales C. Mie mi s-a părut mult mai fragil modul de a defini interfețe in C comparat cu cel din C++, dacă voiam să adăugăm o funcționalitate nouă trebuia să modificăm în multe locuri. Mai era o problemă (dar unrelated cu ce am zis) cu stabilirea regulilor de ownership, pierdeam mult timp pe treaba asta la code review, mai ales când era vorba de stocat string-uri în structs, ceva ce se putea rezolva ușor cu smart pointers și std::string. Iar timpul de recompilare nu e rău în C++ dacă nu faci abuz de templates (dacă folosești boost atunci clar o să ia 5 min o recompilare :laughing:). Dar poate să fie și lipsa mea de experiență.

1 Like

Well, eu am experienta cu ambele, cu C inclusiv pe chestii de kernel linux. E drept ca totusi si eu am mai multa cu C++.

Probabil ca problema trebuie sa se puna atunci astfel: “mai citibil decat ce?” :slight_smile:
Pentru ca n-ai vazut codul kernelului linux implementat in C++ asa ca nu prea ai un termen de comparatie echivalent.

Obiectualul se implementeaza in C cu structuri continand si pointeri la functii, sa fim seriosi, asa ceva nu poate fi mai citibil decat o clasa din C++. Nemaivorbind ca pierzi RAII (si multe altele) si trebuie sa le ‘simulezi’ cu cod greoi.

Cred că aici e cheia, de exemplu dacă comparăm cu un C++ de genul:

template<class C, class Pointed, class T1, class... Args>
constexpr decltype(auto) invoke_memptr(Pointed C::* f, T1&& t1, Args&&... args)
{
    if constexpr (std::is_function_v<Pointed>) {
        if constexpr (std::is_base_of_v<C, std::decay_t<T1>>)
            return (std::forward<T1>(t1).*f)(std::forward<Args>(args)...);
        else if constexpr (is_reference_wrapper_v<std::decay_t<T1>>)
            return (t1.get().*f)(std::forward<Args>(args)...);
        else
            return ((*std::forward<T1>(t1)).*f)(std::forward<Args>(args)...);
    } else {
        static_assert(std::is_object_v<Pointed> && sizeof...(args) == 0);
        if constexpr (std::is_base_of_v<C, std::decay_t<T1>>)
            return std::forward<T1>(t1).*f;
        else if constexpr (is_reference_wrapper_v<std::decay_t<T1>>)
            return t1.get().*f;
        else
            return (*std::forward<T1>(t1)).*f;
    }
}

Atunci da, pot să înțeleg de ce ar zice cineva că C e mai citibil. În schimb dacă comparăm interfețe emulate cu function pointers în C vs. interfețe din C++… nu știu, n-aș mai zice că e așa citibil.
Și mie îmi place C, poate să fie foarte elegant, mai ales cu chestiile adăugate în C11, dar un detaliu pe care l-am observat e că, în general, codul elegant de C se folosește extensii non-standard ale compilatoarelor. Extensii precum statement exprs sau __attribute__((cleanup)) pot să ajute enorm. În C++ vin de-a gata.

C++ e un limbaj foarte ok dacă e folosit ca un C + niște helpers atunci când e nevoie. Dacă se face abuz de ce aduce C++ atunci iese o varză. Dar asta cred că se poate cam în orice limbaj. Problema la C++ pentru mine e că limbajul include prea multe, are niște reguli prin anumite părți ale standardului incredibil de complexe (de exemplu std::launder, sau value categories).

2 Likes

Templateurile permit marirea performantei peste ce poate C. Pe bune. Asa se poate ajunge la performante de fortran, din cauza optimizarilor la compilare. constexpr intamplator are aceasi justificare… si daca totusi e s-o dam pe comparatii de-astea, de ce sa nu bagam niste varza cu macrouri din C? :slight_smile:

1 Like

Da, exemplul clasic aici e cu std::sort, îi e mult mai ușor compilatorului să facă inline la comparator. Asta se aplică la orice funcții care funcționează așa. În C se primește un void* care e total opac.

Behold :laughing:

#define TASK_INIT() static void* f; if(f) goto *f;
#define TASK_YIELD() { __label__ END; f=&&END; return; END: ; }
#define TASK_END() f=0;

void testingSub1() {
    TASK_INIT();
    printf("A"); 
    TASK_YIELD();
    printf("B");
    TASK_YIELD();
    printf("C");
    TASK_END();
}

void testingSub2() {
    TASK_INIT();
    printf("1"); 
    TASK_YIELD();
    printf("2");
    TASK_YIELD();
    printf("3");
    TASK_YIELD();
    printf("4");
    TASK_END();
}

void testingSub3() {
    TASK_INIT();
    while(1) {
        printf("*");
        TASK_YIELD();
    }
    TASK_END();
}

void testing(){
    testingSub1();
    testingSub2();
    testingSub3();
    printf("|"); 
} 

Corutine în C. Source.

1 Like

Se descurca fiindca acel cod indeplineste un scop total diferit de majoritatea aplicatiilor scrise in C++.

O comparatie mai potrivita ar fi GIMP (utilitar grafic scris in C) fata de vreun echivalent scris in C++. Sau cum ai scrie un blog in C & baza de date favorita.

1 Like

Nu se descurca chiar atat de elegant… structuri continand pointeri la structuri continand pointeri la structuri… continand pointeri la structuri continand pointeri la functii.

Un header ales la intamplare: linux/drm_crtc.h at master · torvalds/linux (github.com)

2 Likes

Pentru a scrie un kernel (cel putin in partea de core) nu poti folosi orice compilator si orice librarii pentru ca multe coduri nu ruleaza in absenta unui sistem de operare. Stiu asta de prin facultate cand am scris la nivel de learning un astfel de kernel. Pe atunci lucram in C (djgpp) + asamblare (AT&T) si practic iti faceai totul de la zero (driver tastatura, driver video, management memorie, procese, biblioteci de lucru cu stringuri,… printf, etc). Inclusiv imaginea se construia intr-un anume fel ca sa se poata executa cand ajungea in memorie. In partea evoluata a limbajelor de programare exista elemente si constructii care nu ar functiona sau care nu ar fi controlabile (eg alinierea datelor in memorie). Acum nu stiu, poate lucrurile au mai evoluat dar cel putin la nivel de core nu cred ca se justifica obiectele.

2 Likes

Se poate scrie un kernel si in C++, daca vrei neaparat:

C++ - OSDev Wiki

Majoritatea feature-urilor din C++ sunt doar syntax sugar, n-ar trebui să fie probleme cu compilatul. Există kernels și în Rust (cum e Redox), care la rândul lui are multe features comparat cu C, și ele fiind mai mult syntax sugar.

Hmm, poți detalia aici puțin? Memory alignment cred că funcționează la fel și în C și în C++, altfel n-ar mai fi compatibile.

Un exemplu real poate fi Genode. Se poate considera și Zircon din Fuchsia care (cred că) e o combinație de C, C++ și puțin Rust, unde sigur predomină C++.

1 Like

Nu mai tin minte exact dar stiu ca aliniam manual segmentele la 4k si inclusiv prin cod trebuia sa faci niste trucuri pentru ca unele structuri trebuiau sa stea intr-un anume fel sau la anumite adrese in memorie ca sa functioneze. Lucruri care in mod normal nu te preocupa in aplicatii uzuale. De aia foloseam un C foarte basic pentru ca problema nu era compilatul ci codul generat care trebuia sa poata functiona incarcat la o anumita adresa intr-un spatiu de memorie raw.

2 Likes

Puțin unrelated, dar pentru memory alignment C++ chiar oferă ceva helpers în plus față de C (pe lângă alignas și alignof):

  • std::align - aliniază un pointer la cât se cere dacă se poate, altfel întoarce nullptr
  • std::aligned_storage - Len bytes aliniați la alignment-ul cerut, e util pentru implementat containere unde nu e obligatoriu să se apeleze constructorii la inițializare
  • std::aligned_union - Len bytes care sunt aliniați automat la cât trebuie, astfel încât să se poată stoca n tipuri pe același storage (cu asta se poate implementa un union)
  • std::assume_aligned - ceva mai obscur, probabil e util doar pentru optimizări, spune compilatorului că pointer-ul e aliniat la N
  • std::aligned_alloc - ăsta e și în C11, un mic inconvenient e că la ambele mărimea trebuie să fie multiplu de alignment
3 Likes

Ce e interesant legat de asta este ca inca mai sunt probleme cu aliniamentele in memorie in 2022.

Deobicei, un kernel linux pe x86 e compilat pentru aliniament la 4k, dar chestiile se schimba cand te duci pe alte platforme mai noi.

De exemplu, relativ recent baietii de la Asahi linux (linux pe M1) s-au gasit nevoiti sa modifice un intreg driver ca sa suporte si kernelele compilate pentru 4k (cele pe care ruleaza majoritatea distro-urilor).
Aparent, MacOs-urile pe M1 au un aliniament de 16k by default, ruland in compatibilitate de 4k doar cand ruleaza ceva prin Rosetta.

16k page size este o gramada (pe Linux intra la categoria Huge Page Size) si ma intreb daca exista vreo diferenta in performanta notabila fata de un 4k page size.

Tine de cum accesezi memoria, de-asta e important sa ai lucrurile aliniate corect. Intre C si C++ nu-s diferente in felul cum se face packing la structuri samd. DAR, sunt diferente intre toolchainuri si compilatoare (uneori si arhitectura) care pot sa faca padding diferit in memorie sau sa nu faca padding deloc. Imi amintesc cum injuram niste pragma-uri de packing de la Microsoft acum ceva ani.

Daca tot ce folosesti este compilat la fel din punctul asta de vedere si pentru aceeasi arhitectura, cam ai putea sa folosesti ce vrei tu. Nu cred ca o sa vezi vreodata in ceva kernel Boost, dar in teorie nu ar fi nici un motiv tehnic pentru care nu s-ar putea :slight_smile: Cele mai multe motive sunt filozofice mai mult si se invart in jurul opiniei ca un kernel ar trebui sa fie self-sufficient si sa nu aibe alte dependinte inafara de compilator (well, sort of … ).

Acum, argumentul filozofic ca un kernel trebuie sa fie self-sufficient e doar la runtime ca deja sa compilezi kernelul ai nevoie de o gramada de utilitare (makefile, binutils, openssl, samd) deci aici vad ca se interpreteaza foarte mult. E mai mult o incapatanare sa nu se accepte cod din exterior, deoarece trebuie sa retinem ca linux kernelul e ca o enclava care are propriile reguli, idei si dictator benevolent.

2 Likes