Calling convention na design destructive move nikdy neměla vliv, ani C++ ABI. Pokud přidám do jazyka nějakou feature, tak si můžu dovolit upravit ABI pro potřebu té feature, pokud dřív neexistovala (přidání feature není ABI break). Navíc C++ standard nic jako calling convention nedefinuje, takže z pohledu C++ je platform ABI irelevantní.
Takže problém není úklid na úrovni ABI, problém je ten, že destructive move má corner cases, které nikdo nebyl schopný vyřešit. Takže jsme se dostali do stavu, že tomu říkáme sice move, ale move to vůbec nemusí být, a každý kdo chce implementovat move, musí implementovat taky stav, kdy něco bylo moved... většinou se ta instance prostě vyresetuje do "default constructed" stavu, ale někdy to nejde a to je pak peklo. Navíc je to zbytečný overhead, který umí kompiler odstranit jen když je move constructor a destructor inline (nebo LTO, atd...).
Ale nikdy bych neříkal nikdy. Destructive move je něco co lidi v C++ chcou, a nedají s tím pokoj, protože to je užitečná věc.
Volaný je vůbec ničit ale nemusí. Destructive move znamená to, že ty data se proste opustí a ta instance zanikne, a calling convention tady nemá žádný vliv. Rust to vyřešil naprosto nejlíp, protože move znamená, že se udělá prostě memcpy (takže ty data musí být jak jinak než "movable").
Problém u C++ je, že celá semantika std::move() a &&ref nevynucuje, že k tomu "move" opravdu dojde, a to je vážný problém - compiler by musel dělat mnohem hlubší analýzu kódu než teď, a protože C++ nevyžaduje mít zdrojáky knihoven (stačí hlavičkové soubory), tak zkoumat jak je něco implementované v knihovnách prostě nejde (tady to má rust jednodušší, protože používá jen C ABI a vyžaduje static linking všech rustích závislostí, takže do těch funkcí "vidí").
Další věc je exception safety - co by se tak mělo stát, když chci udělat move argumentu, co předávám funkci, a ještě než dojde k tomu move, tak se výhodí exception? Instance by nezanikla v té funkci a byl by to memory leak? Tady by bylo potřeba mít nějaký flag, který by to hlídal (a myslím, že rust přesně toto dělá).
Ale jak píšu, nikdy bych neříkal nikdy, přecejenom je to C++ a tam je možné všechno :)
Napíšu článek kde ukazují že dokud je za úklid argumentů zodpovědný volající tak to jinak udělat nejde viz příklad 2 - je to marný, je to marný, víra nahrazuje argumenty
Prostě když obj foo přesunu do argumentů funkce bar a ta jej použije a už nepřesune, v jakém kontextu se zavolá destruktor? V kontextu funkce bar? Nebo v kontextu funkce volajíci? Zkuste si to zjistit.
Problém je, že volající neví co bar s foo udělala. Zatímco funkce bar to ví moc dobře
Funkce bar to taky nemusí vědět, když ty data předá dál.
Stdcall nebo Cdecl na to nemá absolutně žádný vliv (toto je v článku slepá ulička) - nastartuj compiler explorer a podívej se, jak se předávají argumenty co používají tyto 2 volací konvence, a zkus si předat třeba std::vector<char> - není v tom absolutně žádný rozdíl, protože stdcall se v tomto případě ani nepoužije. Stdcall je jen pro C ABI - C++ použije cdecl. A takto to je mimochodem i v některých případech v C - třeba printf() nikdy nebude stdcall.
Takže článek je v tomto smyslu zavádějící - mluvit o destructive move a zdůvodňovat to popisování cdecl a stdcall nedává smysl. Toto prostě není ten důvod.
Ondřej Novák - takže pokaždé, když tě tady někdo upozorní na zjevnou chybu v článku budeš na toho člověka reagovat takto? To nebylo myšlené zle, na druhou stranu pokud píšu články o C++, tak na tu kritiku musím být připravený, protože tento jazyk nikdo nezná na 100%.
Takže ještě jednou - calling convention na destructive move nemá absolutně žádný vliv. To, že se tím článek zabývá je chyba a je to zavádějící, když důvody proč to v C++ není jsou úplně jiné.
Jak už tu jiní napsali, calling convention neřeší volání destruktoru, a pokud předám nějakou instanci by value, která má netriviální destruktor, tak se stejně předá jen pointer na ni (a toto je mimiochodem specifikované přimo v C++ standardu). No a to je důvod, proč si zkusit ten compiler explorer - pro líné je taky link:
https://godbolt.org/z/vT4jdTh35
Takže jak se předá std::vector by value? Referencí... O úklid se vždycky stará kdo? Caller...
Nehledě na to, že stdcall už dnes nikde nevyzkoušíš, tak maximálne ve windows, když voláš nějakou DLL funkci. Všechno je cdecl. Všude je cdecl. I když v GCC použiješ stdcall, stejně to bude cdecl. Všechny překladače to znají ale ignorují to.
A není to jen o předání argumentů. ale i o zodpovědnosti za jejich destrukci. K tomu si se ještě nevyjádřil. Tak znova, kde se zavolá destruktor, když foo přesunu do bar pomocí argumentu ve volání ? Až zodpovíš tenhle dotaz, budu reagovat
Za mě by klidně mohlo existovat pravidlo, že callee vždy zajistí destrukci všech "moved" (rvalue) argumentů. Problém jsou Céčkové variadické funkce, kde callee ani neví, co vlastně dostal jako argumenty. Kompilátor by sice mohl takovou situaci detekovat a označit za chybu, ale není to ideální. Možná proto se autoři C++ standardu rozhodli vydat jinou cestou, takže destrukci argumentů zajišťuje caller.
Každopádně souhlasím s názorem výše, že calling conventions nemají vliv na destructive move. Stack cleanup klidně může provádět caller a volání destruktoru callee. Navíc na moderních architekturách se argumenty často vejdou do registrů, takže explicitní stack cleanup ani není potřeba.
Jinak stdcall se stále používá, protože jinak by nešlo volat WinAPI, kde většina funkcí je stdcall. Pozor na to, že cdecl a stdcall jsou záležitost pouze 32-bit x86 (IA-32). Na dnes běžnějším 64-bit x86 (x86-64) a ostatních 64-bit architekturách se zpravidla používá jen jedna calling convention, kde stack cleanup provádí caller, protože jinak nelze implementovat Céčkové variadické funkce.
Microsoft má na 32-bit x86 také ještě thiscall pro member funkce (pokud nejsou variadické). Je to podobné jako jejich oblíbený stdcall, takže stack cleanup provádí callee, jen se navíc "this" pointer předává v ECX registru a ne na stacku s ostatními argumenty.
Všechno je to hezky popsané na Wikipedii: https://en.wikipedia.org/wiki/X86_calling_conventions
K tomu pár věcí, kde vidím problémy.
1) C++ před C++11 prováděl destrukci caller. Proto bylo move navrženo tak jak bylo, o tom je článek. Nešlo to změnit. A je obtížné to změnit i nyní.
-asi by šlo změnit ABI a označovat funkce nějakým flagem, aby se to při linkování nesmíchalo, ale kdyby to bylo jednoduchý, tak už to tam je.
2) Je jedno, jak se argumenty předávají, pointer můžu mít jak v zásobníku tak v registru. Záleží, v jakém kontextu se zavolá destruktor. Pokud v kontextu callee, pak by destructive move bylo standardní součástí C++. Protože to ale dělá caller, tak bez znalosti toho, co s obsahem udělala callee nemůže rozhodnout a musí si nějak poznačit, zda se obsah v callee přesunul/destruoval nebo ne. A než zavádět nějaký rt flagy, zůstalo to označení na programátorovi
Rust to dělá přesně tak, jak napsal Daniel přede mnou. Používá cdecl volací konvenci, kde je za stack cleanup zodpovědný caller a za volání destructoru callee. Takže ano, je možné mít desctructive move i s volací konvencí, kdy se o stack stará caller, protože volání destructoru není spojené s údržbou stacku.
Aby mě někdo nechytal za slovo: ano Rust nemá stabilní ABI, ale aktuálně to tak funguje, o stack se stará caller a o dealokaci callee.
Označil bych to spíš o hybrid.
cdecl znamená, že o úklid parametrů se stará volající a to nejen o vyčištění zásobníků, ale v C++ i o volání destruktorů.
Pokud Rust používá variantum, že volaný zavolá destruktory ale zásobník pak vyčistí volající, pak je to takový hybrid.
Ale v zásadě je to tak jak píšu, tohle jsou ty reálné důvody, proč bylo zvolené určité řešení.
cdecl jako takové řeší jen vyčištění zásobníku, nic neříká o volání destructorů. C++ se rozhodlo, že bude volat destructory těsně před vyčištěním zásobníku v caller contextu, ale nic nebrání tomu volat destructory v callee kontextu, jako to dělá Rust, a pořád to bude cdecl. Nicméně to je slovíčkaření.
C++ už v historii ABI breaky mělo, např. std::string kvůli zrušení copy-on-write a std::list kvůli size() v C++11. Vážně se o možnosti větších ABI breaků uvažovalo na konferenci v Praze a hlasovalo se o tom, ale ABI break boužel neprošel. Podle mého názoru se tím C++ odsoudilo k tomu, že bude v dlouhodobém měřítku stále méně relevantní (Google se už od C++ dost odklání). Zajímavé povídání je tom zde: The Day The Standard Library Died
Co tím chci říct, i C++ je schopné ABI break potenciálně udělat (třeba na druhý pokus...).
velké firmy to dělají běžně. Microsoft má C#, Oracle ma Java, Google bude mít nějaký ten karbon, nebo co
Ale kdepak, Carbon je jen experiment, který (zatím) nemá praktické využití. Google sází mimo jiné na Rust, v Androidu už se s C++ vůbec nepočítá.
This repository has the source code for Comprehensive Rust 🦀, a multi-day Rust course developed by the Android team. The course covers all aspects of Rust, from basic syntax to generics and error handling.
Course Format and Target Audience
The course is used internally at Google when teaching Rust to experienced software engineers. They typically have a background in C++ or Java.
Všechno je cdecl na 64-bit Windows, protože 64-bit Windows měl jen Win64 calling konvenci, až později přišel vectorcall. X86_64 Linux vždycky používal System V ABI, které definuje jen cdecl, která se ukázala asi jako nejlepší.
Stdcall vs cdecl je striktně diskuze o 32-bit x86, nikde jinde stdcall není.
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 53 735×
Přečteno 25 575×
Přečteno 23 820×
Přečteno 22 328×
Přečteno 22 270×