Permalänk
Medlem

Funktionspekararrayer i C

Hejsan!

Jag har en fråga angående C. Jag skulle villja göra en array med funktionspekare. När de är av samma typ har jag förstått hur man gör, och har löst det på detta sätt:

typedef void (*void_func)(void);
typedef int (*int_func)(void);
typedef int (*int_int_func)(int);

void_func Array1[3] = {vfun1, vfun2, vfun3}; //void in, void out
int_func Array2[2] = {ifun1, ifun2}; //void in, int out
int_int_func Array3[1] = {iifun1} //int in, int out

Anropen till de individuella arrayernas funktioner förstår jag:

Array1[n]();

eller

Array3[n](x);

Problemet är att jag skulle helst villja ha dem i en gemensam array så jag enklare kan välja en av dem från samma källa, men förstår inte riktigt om det går att göra en sorts gemensam anropsfunktion utan att ha en massa if-satser eller hur de olika typerna ens skulle kunna blandas i en array?

Är det i så fall enklare och/eller bättre att ha någon sorts av lista? Inte helt bergsäker på dem än, men har väl en grundläggande idé om hur de fungerar.

Permalänk
Medlem
Skrivet av Chmat76:

Hejsan!

Jag har en fråga angående C. Jag skulle villja göra en array med funktionspekare. När de är av samma typ har jag förstått hur man gör, och har löst det på detta sätt:

typedef void (*void_func)(void);
typedef int (*int_func)(void);
typedef int (*int_int_func)(int);

void_func Array1[3] = {vfun1, vfun2, vfun3}; //void in, void out
int_func Array2[2] = {ifun1, ifun2}; //void in, int out
int_int_func Array3[1] = {iifun1} //int in, int out

Anropen till de individuella arrayernas funktioner förstår jag:

Array1[n]();

eller

Array3[n](x);

Problemet är att jag skulle helst villja ha dem i en gemensam array så jag enklare kan välja en av dem från samma källa, men förstår inte riktigt om det går att göra en sorts gemensam anropsfunktion utan att ha en massa if-satser eller hur de olika typerna ens skulle kunna blandas i en array?

Är det i så fall enklare och/eller bättre att ha någon sorts av lista? Inte helt bergsäker på dem än, men har väl en grundläggande idé om hur de fungerar.

Kanske det här är något? Funktionspekare C

Visa signatur

OS: MacOS/ Windows 10 Pro 64-bit MB: ASUS-Z97-A CPU: i7 4790k
NÄTAGG: EVGA SUPERNOVA G2
RAM: 32768 MiB GPU: 1070 FTW Chassi: Fractal Design R4
MBP 13" i5 | 256GB | 16GB RAM | MID 2014

Permalänk

@Chmat76:

Nu får någon som är bättre på C än jag gärna rätta mig. Men om du vill ha en array med alla funktionspekarna i så måste deras signatur vara lika d.v.s. de kan inte olika returtyper, olika antal argument eller argument av olika typer.

Om man nu verkligen vill ha allt i samma array så är det sätt som jag kan tänka ut på rak hand att gör allt generiskt.

enum data_type { VOID, INT_16, INT_32, INT_64, FLOAT_16, FLOAT_32, FLOAT_64 } typedef void (*generic_func)(void *, data_type, int, void *, data_type *, int *); void foo( void * in_data, data_type in_type, int in_number_of_elements, void * out_data, data_type * out_type, int * out_number_of_elements)

Om man gör så bör man kunna ha alla funktionspekarna i samma array men jag är väldigt osäker på om jag tycker att det är en bra ide.

Permalänk
Medlem

Du definierar en struct innehållande funktionspekare av de olika typerna och gör en array av denna struct. Du kan inte ha flera typer i samma array.

Permalänk
Datavetare

Ett stort problem i detta fall är att du har funktioner som skiljer sig i returtyp.

Annars kan ju alltid från anropssidan behandla alla funktionspekare som den version som tar flest argument

int_int_func Array[] = { (int_int_func)ifun1, (int_int_func)ifun2, iifun1 };

C-standarden säger att det är anropande kontext som både allokerar utrymme för argument och städar upp, så att anropa ifun1 via en pekare där man lagt med ett argument som den anropade funktionen inte använder är helt OK.

Ofta när man använder polymorfism i C gör man det genom att funktionspekarna är del av instansens tillstånd (till skillnad från typisk OOP där funktionspekarna skulle vara fixerade av typen). För att slippa hantera fallet där en funktionspekare är NULL använder man sig ofta av funktioner med standardresultat (t.ex. gör inget och returnerar "success" eller misslyckas alltid oavsett argument).

Så anrop till Array[0](123) ovan är helt i sin ordning, mottagaren ifun1 kommer ge ett svar och helt ignorera argumentet.

Funktioner som inte returnerar något (void) kommer resultera i totalt odefinierat returvärde om de skulle castas till int_int_func eller int_func, det som kommer tillbaka då i fallet x86 är det som råkar ligga i register eax.

Ett annat alternativ är att stoppa in alla olika typer för dina funktionspekare i en union, men i det läget behövs också ett fält som talar om vilken av pekarna i unionen som är den man borde använda.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Medlem

@Yoshman:

Jag har gjort som du skrev och lyckades inkludera int fun (void) i samma array som int fun (int). Min kompilator gillar inte alls om jag försöker kasta en void fun (void) funktion till int_func eller int_int_func, men om returtypen är densamma går det an!

Att göra det generiskt verkar rörigt att fixa, så jag tror att jag nöjer mig med att skapa två arrayer och sedan välja mellan dem.

Intressant att man kan skicka argument som ignoreras. I många andra språk är det totalförbjudet, typ Java.

Jag har iallafall lärt mig något! Tackar allesammans!

Permalänk
Skrivet av Yoshman:

C-standarden säger att det är anropande kontext som både allokerar utrymme för argument och städar upp, så att anropa ifun1 via en pekare där man lagt med ett argument som den anropade funktionen inte använder är helt OK.

...

Så anrop till Array[0](123) ovan är helt i sin ordning, mottagaren ifun1 kommer ge ett svar och helt ignorera argumentet.

Nej,

Standarddokumentet för C säger att om du har prototyper så SKALL antalet argument i anropet stämma med antalet i prototypen. Bara för att det, för en viss kompilator, råkar funka att skicka extra parametrar betyder det inte att det gör det för alla. Ur standardens synvinkel är det inte längre ett C-program, eftersom det inte följer C-standarden, och då får vad som helst hända när du kör det. Till exempel får det hoppa upp krokodiler ur tangentbordet och bita dig i näsan, men du kanske har tur du...

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Nej,

Standarddokumentet för C säger att om du har prototyper så SKALL antalet argument i anropet stämma med antalet i prototypen. Bara för att det, för en viss kompilator, råkar funka att skicka extra parametrar betyder det inte att det gör det för alla. Ur standardens synvinkel är det inte längre ett C-program, eftersom det inte följer C-standarden, och då får vad som helst hända när du kör det. Till exempel får det hoppa upp krokodiler ur tangentbordet och bita dig i näsan, men du kanske har tur du...

Mitt svar var strippat på rätt mycket detaljer, för någon som de detaljer jag skriver nedan på något sätt är relevant för lär de knappast behöva ställa den typ av fråga som denna tråd handlar om.

Rent strikt så säger inte ANSI-C något om detta, men rent praktiskt så har alla moderna OS en binär specifikation för C kallad Application Binary Interface, ABI. I ABI står exakt vilka register som används för argument, när stacken används för argument samt vilken ordning man binder argument till register/plats på stacken. Bind-ordningen är viktig för att det jag skrev ska fungera, den måste vara så att argument stoppas in i register från vänster och de som måste gå på stacken pushas höger till vänster (så det mest vänsterjusterade argumentet ligger högst upp). Windows, Linux, OSX, *BSD (faktum att alla *NIX), VxWorks, QNX, ... fungerar alla på detta sätt i normalfallet så inte speciellt icke-portabelt antagande.

Som exempel på ABI, Windows ABI för 64-bitars x86 hittar du här och alla *NIX jag känner till följer denna ABI. Notera att alla C-kompilatorer på en plattform måste följa denna binära specifikation, om de inte skulle göra det vore det omöjligt att ha kod i form av förbyggda bibliotek. Notera och att C++ inte har en motsvarande binär specifikation, det är anledningen till att Microsoft uppfunnit saker som OLE, COM och C++/CX för utan dessa går det inte att leverera förkompilerad kod som exporterar klasser.

Är det slumpen att alla OS valt att binda argument på samma sätt (även om det skiljer lite i exakt vilka/hur många register man använder)? Knappast, tänk på stdarg.h (som är en del av ANSI-C) och hur det vore hopplöst att implementera detta utan att man i absolut detalj specificeras hur argument binds till register/stack, vem som allokerar utrymmet och vem som städar upp detta.

Skriver man ANSI-C stämmer det jag skrev i inlägget ovan på alla OS jag känner till i alla fall, det "råkar" inte bara fungera på vissa kompilatorer utan ABI säkerställer att det fungerar.

Det är möjligt att skjuta sig i foten om man använder kompilatorspecifika utökningar som gör att man frångår den normala ABI (det många kallar C-calling convention). Koden nedan kommer krascha då jag satt "__stdcall" (Pascal calling convention, den som blir anropad städar stacken)

#include <stdio.h> #define ARR_SZ(arr) (sizeof arr / sizeof *arr) typedef int(*action_f)(int, int); int foo() { return 0; } // __stdcall är en extension som de flesta C/C++ kompilatorer för Windows har int __stdcall bar(int x) { // anropsstacken kan mycket väl vara havererad när denna funktion avslutas return x; }ind int main(int argc, char *argv[]) { action_f a[] = { (action_f)foo, (action_f)bar }; for (int i = 0; i < ARR_SZ(a); i++) { printf("f(%d), %d\n", i, a[i](10, 20)); } }

Anledningen är att anropar antar "C-call" och kommer därför själv att städa upp, bar() använder "Pascal-call" och kommer därför städa som om det verkligen bara vore ett argument. Pascal calling convention är på flera sätt sämre en C calling convention, detta är bara en av anledningarna. Funktionerna i stdarg.h hade inte gått att göra med Pascall-calls, anledningen att __stdcall överhuvudtaget finns på Windows är att Windows APIet har av någon outgrundlig anledning Pascall calling convention.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk

Jag hittade en länk till en C99-standard på nätet. Rent strikt så säger ANSI C precis detta om funktionsanrop:

3.3.2.2 Function calls

...
Constraints

The expression that denotes the called function/29/ shall have type
pointer to function returning void or returning an object type other
than array.

If the expression that denotes the called function has a type that
includes a prototype, the number of arguments shall agree with the
number of parameters.
Each argument shall have a type such that its
value may be assigned to an object with the unqualified version of the
type of its corresponding parameter.

Semantics

A postfix expression followed by parentheses () containing a
possibly empty, comma-separated list of expressions is a function
call. The postfix expression denotes the called function. The list
of expressions specifies the arguments to the function.

If the expression that precedes the parenthesized argument list in
a function call consists solely of an identifier, and if no
declaration is visible for this identifier, the identifier is
implicitly declared exactly as if, in the innermost block containing
the function call, the declaration

extern int identifier();

appeared. /30/

An argument may be an expression of any object type. In preparing
for the call to a function, the arguments are evaluated, and each
parameter is assigned the value of the corresponding argument./31/ The
value of the function call expression is specified in $3.6.6.4.

If the expression that denotes the called function has a type that
does not include a prototype, the integral promotions are performed on
each argument and arguments that have type float are promoted to
double. These are called the default argument promotions. If the
number of arguments does not agree with the number of parameters, the
behavior is undefined.
If the function is defined with a type that
does not include a prototype, and the types of the arguments after
promotion are not compatible with those of the parameters after
promotion, the behavior is undefined. If the function is defined with
a type that includes a prototype, and the types of the arguments after
promotion are not compatible with the types of the parameters, or if
the prototype ends with an ellipsis ( ", ..." ), the behavior is
undefined.

Det jag reagerade mot var att du skriver att det är helt i sin ordning att skicka en extra parameter eller två och i samma inlägg säger "C-standarden". Det du beskriver är inte C-program. Jag säger inte att det inte funkar, men det är inte ett "standard compliant C program" och då finns det inte längre några som helst garantier för att ditt program skall fungera. Det är inte ovanligt att kompilatorna drar nytta av att ditt har program har "undefined behavior" och optimerar aggressivare är vad standarden egentligen tillåter. Att det råkar funka att göra är inte alls förvånande med tanke på C:s calling convention, men det är inte längre ANSI C.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Jag hittade en länk till en C99-standardpå nätet. Rent strikt så säger ANSI C precis detta om funktionsanrop:

3.3.2.2 Function calls

...
Constraints

The expression that denotes the called function/29/ shall have type
pointer to function returning void or returning an object type other
than array.

If the expression that denotes the called function has a type that
includes a prototype, the number of arguments shall agree with the
number of parameters.
Each argument shall have a type such that its
value may be assigned to an object with the unqualified version of the
type of its corresponding parameter.

Semantics

A postfix expression followed by parentheses () containing a
possibly empty, comma-separated list of expressions is a function
call. The postfix expression denotes the called function. The list
of expressions specifies the arguments to the function.

If the expression that precedes the parenthesized argument list in
a function call consists solely of an identifier, and if no
declaration is visible for this identifier, the identifier is
implicitly declared exactly as if, in the innermost block containing
the function call, the declaration

extern int identifier();

appeared. /30/

An argument may be an expression of any object type. In preparing
for the call to a function, the arguments are evaluated, and each
parameter is assigned the value of the corresponding argument./31/ The
value of the function call expression is specified in $3.6.6.4.

If the expression that denotes the called function has a type that
does not include a prototype, the integral promotions are performed on
each argument and arguments that have type float are promoted to
double. These are called the default argument promotions. If the
number of arguments does not agree with the number of parameters, the
behavior is undefined.
If the function is defined with a type that
does not include a prototype, and the types of the arguments after
promotion are not compatible with those of the parameters after
promotion, the behavior is undefined. If the function is defined with
a type that includes a prototype, and the types of the arguments after
promotion are not compatible with the types of the parameters, or if
the prototype ends with an ellipsis ( ", ..." ), the behavior is
undefined.

Det jag reagerade mot var att du skriver "ANSI-C" i samma inlägg som du postar något som definitivt inte är ANSI C. Jag säger inte att det inte funkar, men det är inte ett "standard compliant C program" och då finns det inte längre några som helst garantier för att ditt program skall fungera. Det är inte ovanligt att kompilatorna drar nytta av att ditt har program har "undefined behavior" och optimerar aggressivare är vad standarden egentligen tillåter. Att det råkar funka att göra är inte alls förvånande med tanke på C:s calling convention, men det är inte längre ANSI C.

Sant! Ur ett rent ANSI-C perspektiv har du rätt.

Dock är ANSI-C utan ett ABI är värdelöst, vilket betyder att även om ANSI-C inte definierar ett beteende så är beteendet i praktiken väldefinierat i alla moderna OS så länge som man håller sig till syntaktiska element som är definierad i ANSI-C.

Så rent praktiskt kommer det inte bara "råka" fungera på Windows, Linux, OSX och en lång rad andra OS därför att de alla har ABIer för x86, x86_64, Aarch32, Aarch64, PPC etc som garanterar att det kommer fungera. Finns inga "aggressiva" optimeringar som kan ändra detta utan att rena OS-anrop + existerande program/bibliotek havererar, vilket också betyder att det fungerar oavsett kompilatortillverkare.

Det som är kritiskt är att de argument man faktiskt väljer att ha med i den funktion man tar adressen på exakt matchar de N första argumenten (räknat från vänster) på den funktionstyp man anropar funktionen via.

Edit: @Ingetledigtnamn du ska definitivt ha en +1 på ditt svar, men inser vilken extrem begränsning det skulle vara om man faktiskt inte skulle kunna göra det jag skrev i mitt första svar så kollad lite om existerande grundläggande kod faktiskt klarar att inte göra sådana casts.

Precis som jag misstänkte så förutsätter t.ex. Gnomes glib, det mest grundläggande biblioteket i hela Gnome som fungerar på alla idag existerande OS som har en desktop. T.ex. här.

För att gå till något ännu mer grundläggande, glibc, så förutsätter implementationen av qsort att cast från funktion som tar 3 argument till funktion som tar 2 argument är "safe". Lär knappast finnas något modernt OS där glibc inte är kompilerat för.

Så som jag redan skrivit, i praktiken fungerar det precis som i min första post för helt fundamentala bibliotek som används på miljarder enheter skulle sluta fungera korrekt om så inte var fallet.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk

@Yoshman:

Du lever i en skyddad verkstad. Som arbetande i embedded-industrin skulle jag defintivt inte kalla ANSI C utan ett ABI värdelöst. Det finns ännu fler miljarder devices som inte har det du kallar ett modernt OS än devices som har det. Jag lovar, vi pratar magnituder fler. En stor del av dessa programmeras i C utan varken OS eller ABI. Tänjer man på standardens gränser är det långt ifrån säkert att det som råkar funka med en kompilator kommer funka likadant med en annan. Det är inte ens säkert när man följer standarden

Permalänk
Datavetare

@Ingetledigtnamn: Om du jobbar med små inbyggda system, typ 8/16-bitars mikrokontrollers, borde du redan vara smärtsamt medveten om att man knappast kan lita på att sådana system följer ANSI C. Oavsett om de gör det eller ej måste det finnas en ABI (formell eller ad-hoc), utan en sådan är det inte ens möjligt att skriva en kompilator. En av de viktigaste uppgifterna för en ABI är att specificera de CPU-specifika detaljerna kring hur man gör ett funktionsanrop.

Har jobbat med en kompilatorer för DSPer där man ibland undrar om de som skrivit kompilatorn överhuvudtaget testat något mer komplicerat än "Hello world", där var ett stort problem just att de inte heller följde ANSI-C, vad man i praktiken hade var ett "C-likt" språk. Finns andra exempel där tillverkaren inte ens hävdar att det är rent C utan "based on ANSI-C" som t.ex. Neuron C (Neuron C används i klass av 8-bitars mikrokontrollers som är förvånansvärt kraftfulla).

Det sagt, om ABI säger att man ska köra Pascall-calling convention eller något totalt vansinnigt som att register/stack-användning är totalt olika beroende på antal argument så skulle det jag skrev i mitt första inlägg bli helt fel (men det skulle också omöjliggöra stdarg.h vilket är en del av ANSI-C). Än så länge har jag aldrig stött på ett system där det inte skulle gå att anropa en funktion som tar, säg ett argument av typ "int" via en pekare med typen "funktion som tar två argument av typen int".

Tar man 32/64-bitars CPUer så har de i praktiken en ABI som alla följer, ARM definierar en för sina modeller, MIPS gör samma sak, PowerPC har det i ISA-specifikationen, SPARC har ju till och med namngett register så det bara finns ett rimligt sätt att hantera argument. Alla dessa har en ABI som gör det jag postade väldefinierat och korrekt.

Det jag skrev i första inlägget är som sagt odefinierat i ANSI-C, men inte p.g.a det du refererar till. Läs exakt vad de meningar från C11 standarden (som för tillfället implementeras av noll kompilatorer, men C89 säger ungefär samma sak fast med färre ord) som du markerade säger:

"If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters."

Vad detta säger är att detta inte är tillåtet

int foo(int x, int y); /* prototyp för 'foo' */ void bar() { foo(1); foo(1,2,3); }

"If the expression that denotes the called function has a type that does not include a prototype, the integral promotions are performed on each argument and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not agree with the number of parameters, the behavior is undefined."

Vad detta säger är att detta kommer bli fel

i någon C-fil finns, ingen "static" så extern länkning double foo(float x, float y) { return x * y; } ... i en annan fil anropar jag "foo" utan att ha en prototyp, detta kompilerar då det inte finns något formellt fel void bar() { /* argumenten kommer skickas via heltalsregister alt. * som heltal på stacken ("promotion" är till "int" i detta fall), * men mottagaren förväntar sig 2st SP flytta, inte 3 intsl!!! */ char x = 10; double ret = foo(x, 20, 30); ... }

Standarden säger ändå att det jag skrev initialt är odefinierat (av ANSI-C), raden i fråga är denna från kapitel J.2 "Undefined behavior" i C11

"A pointer is used to call a function whose type is not compatible with the referenced type"

Och det p.g.a definitionen av vad som är "compatible", även om det finns ett "loophole" i form av att funktioner som använder sig av K&R stil inte tar med sin argumentlista i jämförelsen över vad som är "compatible". Så skulle kunna skriva om exemplet i K&R stil och på så sätt vara ANSI-C kompatibel, rätt uppenbart att detta är ett område man helt enkelt inte detaljgranskat.

I korthet: i praktiken kommer det fungera på var enda kompilator för var enda CPU-arkitektur där det är möjligt att göra indirekta anrop. Finns det någon ASIC/FPGA som inte stödjer indirekta anrop går det överhuvudtaget inte att jobba med pekare till funktioner så problemställningen är irrelevant.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk

Du har rätt i att de meningar jag markerade inte diskvalificerar ditt ursprungliga program, men jag hade inte tillgång till C99-standarden hemma och den paragrafen jag letade efter (6.5.2.2, paragraf 9) fanns inte i C89-standarden jag hittade på nätet. Som du säger måste typen på den anropade funktionen och typen på pekaruttrycket vara kompatibla. Annars är det "undefined behavior" och då kan det hända konstiga saker.

Sedan skulle jag tro att vi lägger olika betydelse i termen ABI och att det orsakat en del förvirring mellan oss. När jag säger (skriver) ABI betyder det för mig att någon (typiskt chipleverantören), för att säkerställa interoperabilitet, har specificerat saker som filformat och calling convention som kompilatorer för den chipfamiljen skall använda. För en chipfamilj där denna specifikation saknas är det inte ovanligt att kompilatortillverkarna valt olika calling convention. Av ditt senaste inlägg att döma använder du termen ABI för det jag kallar calling convention. Och ja, en kompilator måste följa en calling convention, annars funkar ingenting, men den måste nödvändigtvis inte följa ett ABI. Kör man på "bare metal" och länkar mot bibliotek som följer samma calling convention går det alldeles utmärkt. (Den vanligaste anledningen till inte följa ett ABI är att ABI:t har tillkommit i efterhand och där valde en annan calling convention än den som kompilatorn redan använde.) Så när jag skriver att en massa devices programmeras i C utan vare sig OS eller ABI menar jag just att denna specifikation saknas, inte att man inte följer en calling convention. Har man å andra sidan din tolkning av ABI måste mitt förra inlägg framstå som ren rapakalja

Jag tror vi är helt överens om att det du gör funkar i praktiken, men dina funktionpekarkonverteringar gör att ditt program inte längre följer ANSI C.

Permalänk
Datavetare

@Ingetledigtnamn: en viss specifikt kort kan ha olika ABI, även på samma OS. T.ex. har det typiska ABI som används ändrats för ARM körandes under Linux vid ett tillfälle (fanns två olika profiler och ARM rekommenderade att man byter till den nyare). När man konfigurerar kompilatorn måste man även ställa in vilken ABI man ska använda. I GCC går det via switchar att stödja båda, men det är bara vettigt om man kors-bygger. För en kompilator som ska bygga för den lokala maskinen är det ingen poäng att stödja någon annan ABI än den som systemet använder. Rent tekniskt skulle man kunna köra något totalt bare-metal med "fel" ABI, men det betyder också att man måste hantera systemanrop själv för det är omöjligt att använda några existerande bibliotek.

Calling-convention är en del av ABI, ABI innehåller även saker som hur namn på funktioner förhåller sig till länksymboler, om det ska finnas saker som "red-zone", etc.

Och det går faktiskt att göra det på ett sätt som inte bryter ANSI-C så här

typedef int(*action_f)(int, int); /* K&R function definitions */ int foo(x) int x; { return x; } int bar(x, y) int x; int y; { return x + y; } int main() { action_f a[] = { foo, bar }; a[0](10, 20); a[1](10, 20); }

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk

@Yoshman:

Bra försök, men...

Under 6.7.5.3 Function declarators säger paragraf 15: "For two function types to be compatible, both shall specify compatible return types." vilket du har, men sedan kommer "If one type has a parameter type list and the other type is specified by a function definition that contains a (possibly empty) identifier list, both shall agree in the number of parameters, and the type of each prototype parameter shall be compatible with the type that results from the application of the default argument promotions to the type of the corresponding identifier."

Så jag tror fortfarande inte att det är program i ANSI C.

Permalänk
Datavetare

@Ingetledigtnamn: Läs fotnot 127

"If both function types are ''old style'', parameter types are not compared."

Så det är ANSI-C då K&R är "old style" så det enda som spelar roll för att funktionspekare ska anses vara "compatible" är att de ha samma returtyp. Man ser att gcc "vet" detta då jag kompilerar med "warnings as errors" och det behövs ändå ingen explicit cast av funktionspekarna.

Är ju helt uppenbart att man inte detaljgranskat detta i standardarbetet, gör man det lär det bli explicit tillåtet då så mycket väldigt fundamental kod beror av att det fungerar som jag skrev i första inlägget.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

@Ingetledigtnamn: Läs fotnot 127

"If both function types are ''old style'', parameter types are not compared."

Så det är ANSI-C då K&R är "old style" så det enda som spelar roll för att funktionspekare ska anses vara "compatible" är att de ha samma returtyp. Man ser att gcc "vet" detta då jag kompilerar med "warnings as errors" och det behövs ändå ingen explicit cast av funktionspekarna.

Funktionerna foo och bar må vara kompatibla, men det är inte de typerna vi pratar om här. Det är typen på den anropade funktionen och typen på uttrycket in funktionsanropet. Typen för foo är inte kompatibel med action_f.

Edit: Jag ser redan vad du kommer svara på detta. Jag får väl ge mig. Man kan göra ditt trick i ANSI C, men sedan är frågan om du med hedern i behåll kan kalla det ANSI C om du kör med K&R-deklarationer.

Edit igen: Som paragrafryttare måste jag peka ut att fotnoten säger bara att man inte skall jämföra typerna, den säger inte att man inte skall bry sig om antalet argument..

Permalänk
Skrivet av Yoshman:

Är ju helt uppenbart att man inte detaljgranskat detta i standardarbetet, gör man det lär det bli explicit tillåtet då så mycket väldigt fundamental kod beror av att det fungerar som jag skrev i första inlägget.

Intressant, vi kommer till helt olika slutsats.

När jag läste detta tidigare reagerade jag inte över vad du egentligen skriver här. Tänker de till borde de snarare att täppa till luckan. Om man på andra ställen i standarden explicit säger att antalet argument i funktionsanropet skall stämma med antalet parametrar funktionen tar, tror du då att de har glömt att ge dig en lucka där du kan våldföra dig på typsystemet? Språket har redan en mekanism där du själv kan välja hur många parametrar du vill skicka till en funktion, den heter varargs.

Standarddokumentet lägger sig inte i hur kompilatorn hanterar parametrarna, det står uttryckligen. Standarden förutsätter bara att argumentens värden dyker upp i parameterna och hur det går till är upp till kompilatorn att lösa. Jag som kompilatorimplementatör kan till exempel välja att ha olika calling convention beroende på om det är ett udda eller jämnt antal parametrar (och särbehandla varargs) utan att bryta mot standarden. Om du vill att C-standarden skall bestämma att ditt trick skall fungera, då måste den specificera saker som ligger utanför språket och dess semantik och hittills har den inte lagt sig i implementationsdetaljer.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Intressant, vi kommer till helt olika slutsats.

När jag läste detta tidigare reagerade jag inte över vad du egentligen skriver här. Tänker de till borde de snarare att täppa till luckan. Om man på andra ställen i standarden explicit säger att antalet argument i funktionsanropet skall stämma med antalet parametrar funktionen tar, tror du då att de har glömt att ge dig en lucka där du kan våldföra dig på typsystemet? Språket har redan en mekanism där du själv kan välja hur många parametrar du vill skicka till en funktion, den heter varargs.

Standarddokumentet lägger sig inte i hur kompilatorn hanterar parametrarna, det står uttryckligen. Standarden förutsätter bara att argumentens värden dyker upp i parameterna och hur det går till är upp till kompilatorn att lösa. Jag som kompilatorimplementatör kan till exempel välja att ha olika calling convention beroende på om det är ett udda eller jämnt antal parametrar (och särbehandla varargs) utan att bryta mot standarden. Om du vill att C-standarden skall bestämma att ditt trick skall fungera, då måste den specificera saker som ligger utanför språket och dess semantik och hittills har den inte lagt sig i implementationsdetaljer.

Den uppenbara ändringen man borde göra i standaren är att skriva att det är väldefinierat att göra en cast av en funktion med lika många eller färre är argument än den funktionspekare man castar till förutsatt att alla ingående argument matchar. Finns ingen rimligt anledning varför det inte alltid skulle fungera, bl.a. för det är en bieffekt av att standarden kräver stdarg.h stöd och för att det är något som används väldigt ofta redan.

Lycka till att implementera den calling convetion du föreslår som något man borde få göra och ändå stödja stdarg.h. Finns inte heller någon rimligt implementation med Pascal-calling convention som fixar stdarg.h, i alla fall inte i C11 sedan trådar infördes, i tidigare versionen skulle man kunna lösa det med lite globalt tillstånd.

Angående fotnoten, bryr man sig inte om typerna är det meningslöst att bry sig om antalet. Ett exempel är uint64_t på 32-bitars CPUer som kommer fungera som om det var två argument, att då räkna utan att bry sig i faktisk typ ger absolut ingenting.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Den uppenbara ändringen man borde göra i standaren är att skriva att det är väldefinierat att göra en cast av en funktion med lika många eller färre är argument än den funktionspekare man castar till förutsatt att alla ingående argument matchar. Finns ingen rimligt anledning varför det inte alltid skulle fungera, bl.a. för det är en bieffekt av att standarden kräver stdarg.h stöd och för att det är något som används väldigt ofta redan.

Lycka till att implementera den calling convetion du föreslår som något man borde få göra och ändå stödja stdarg.h.

Nej, stdarg kräver inte alls att kompilatorerna gör som du vill. Standarden reglerar att saker och ting skall fungera, inte hur det skall implementeras. Ja, de flesta kompilatorerna gör på liknande sätt, men det betyder inte att kompilatorer måste göra likadant. Att du har hittat en kryphål i standardens formuleringar, så du kan gå runt de paragrafer som explicit förbjuder det du gör, betyder inte att det är tänkt att det skall fungera.

Varför skulle det vara ett problem att fixa stdarg? Jag skrev ju redan i förra inlägget att vi skulle särbehandla det (ok, jag skrev varargs, men jag började programmera C före 89 och då hette det varargs). Man kan exempelvis skicka med en extra parameter före de andra argumenten som talar om vilken calling convention som används. Sedan är det upp till va_*-makrona att välja rätt calling convention. Det är inte ovanligt att kompilatorerna skickar extra parametrar. Om vi tar C++ som exempel är det ganska vanligt att kompilatorn skickar en extra flagga till konstructorn och destruktorn som reglerar huruvida minne skall allokeras eller deallokeras. Kodstorleken minskar om man kan anropa Foo(..., true) instället för att anropa Foo(operator new(sizeof(Foo), ...) på alla ställen Foo-objekten inte redan har färdigallokerat minne. Sedan skall vi inte glömma this-pekaren, den finns inte heller i parameterlistan, men dyker på något magiskt sätt upp i den anropade funktionen.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Nej, stdarg kräver inte alls att kompilatorerna gör som du vill. Standarden reglerar att saker och ting skall fungera, inte hur det skall implementeras. Ja, de flesta kompilatorerna gör på liknande sätt, men det betyder inte att kompilatorer måste göra likadant. Att du har hittat en kryphål i standardens formuleringar, så du kan gå runt de paragrafer som explicit förbjuder det du gör, betyder inte att det är tänkt att det skall fungera.

Varför skulle det vara ett problem att fixa stdarg? Jag skrev ju redan i förra inlägget att vi skulle särbehandla det (ok, jag skrev varargs, men jag började programmera C före 89 och då hette det varargs). Man kan exempelvis skicka med en extra parameter före de andra argumenten som talar om vilken calling convention som används. Sedan är det upp till va_*-makrona att välja rätt calling convention. Det är inte ovanligt att kompilatorerna skickar extra parametrar. Om vi tar C++ som exempel är det ganska vanligt att kompilatorn skickar en extra flagga till konstructorn och destruktorn som reglerar huruvida minne skall allokeras eller deallokeras. Kodstorleken minskar om man kan anropa Foo(..., true) instället för att anropa Foo(operator new(sizeof(Foo), ...) på alla ställen Foo-objekten inte redan har färdigallokerat minne. Sedan skall vi inte glömma this-pekaren, den finns inte heller i parameterlistan, men dyker på något magiskt sätt upp i den anropade funktionen.

C++ är inte relevant att jämföra med, där har man aldrig lyckats enats om en binärstandard ens på enskilda plattformar. Microsoft har just av den anledningen skapat OLE, COM och senast C++/CX. Just this är ett icke-problem, den är ju första argumentet även om den som anropar råkar skriva det argumentet framför punktoperatorn så absolut inget magiskt med det argumentet.

Nu är detta lite OT, men varför skulle en vettig implementation av en ctor ta extra flaggor för minnesallokering? I C++ kan man överlagra new/delete för sina typer, om konstruktion ska ta hänsyn till sådant har man ett rätt komplicerad design. Normala C++ kompilatorer delar upp detta i två steg, första steget är att på något sätt allokera minne för objektet (på stacken om det är en automatisk variabel, från heapen med icke-överlagrad "new" eller från custom "new" annars), steg två är att anropa ctor där första argumentet är en pekare till objektets minne (även ctor har this associerad med sig), en ctor räcker då oavsett hur minnet var allokerat utan att några magiska flaggor skickas med.

Vad jag menar är att eventuella problem en kompilatortillverkare skulle kunna tänkas få om standarden explicit tillät cast av funktion med färre argument till den funktionspekare man castar till måste man ändå hantera om man ska implementera stdarg.h. Visst kan man göra det du beskriver, men det är en vansinnig design då den tar mer overhead (och folk lägger väldigt mycket krut på att få bort enstaka instruktioner ur funktions-prologer/epiloger) än att göra det 100% av alla ABIer i praktiken gör, d.v.s. gör en lista med heltalsregister, en lista med flyttalsregister, vissa mikrokontrollers har en lista med fixa minnespositioner (är OK om det bara finns en CPU-kärna och ingen möjlighet till trådar), där man från fronten av listan tar första registret/positionen som ännu inte används och använder det för respektive argument (från vänster). Tar det slut på register pushar man resten på stacken (från höger så argumenten ligger vänster->höger på stacken, är ju LIFO).

Tittar man lite pragmatiskt på detta så kvittar det faktiskt vad ANSI-C säger om just det denna tråd handlar om. Det jag skriver i mitt första inlägg är i praktiken korrekt då allt för mycket existerande kod beror av beteendet och använder man det mest rimliga/effektiva/enkla sättet för att få argument från den som anropar till den som blir anropad får man på köpet stöd för denna typ av cast. Och ANSI-C utan ABI är värdelös då det inte är möjligt att allokera minne, inte möjligt att använda anrop mot OS, inte möjligt att anropa kod skrivet i assembler, faktiskt inte möjligt att göra några anrop alls utan ett ABI.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Nu är detta lite OT, men varför skulle en vettig implementation av en ctor ta extra flaggor för minnesallokering? I C++ kan man överlagra new/delete för sina typer, om konstruktion ska ta hänsyn till sådant har man ett rätt komplicerad design. Normala C++ kompilatorer delar upp detta i två steg, första steget är att på något sätt allokera minne för objektet (på stacken om det är en automatisk variabel, från heapen med icke-överlagrad "new" eller från custom "new" annars), steg två är att anropa ctor där första argumentet är en pekare till objektets minne (även ctor har this associerad med sig), en ctor räcker då oavsett hur minnet var allokerat utan att några magiska flaggor skickas med.

Det är en uppenbar kodstorleksoptimering och den kan dessutom ha positiva effekter på cacheutnyttjandet. Självklart anropar konstruktorn operator new på samma sätt som görs i ditt första steg, men det räcker att man gör det på ett ställe istället för att det behöver göras på alla ställen där du gör new på ett objekt. I din värld är kodstorleken gratis, men så är det inte överallt.

Skrivet av Yoshman:

Vad jag menar är att eventuella problem en kompilatortillverkare skulle kunna tänkas få om standarden explicit tillät cast av funktion med färre argument till den funktionspekare man castar till måste man ändå hantera om man ska implementera stdarg.h. Visst kan man göra det du beskriver, men det är en vansinnig design då den tar mer overhead (och folk lägger väldigt mycket krut på att få bort enstaka instruktioner ur funktions-prologer/epiloger) än att göra det 100% av alla ABIer i praktiken gör, d.v.s. gör en lista med heltalsregister, en lista med flyttalsregister, vissa mikrokontrollers har en lista med fixa minnespositioner (är OK om det bara finns en CPU-kärna och ingen möjlighet till trådar), där man från fronten av listan tar första registret/positionen som ännu inte används och använder det för respektive argument (från vänster). Tar det slut på register pushar man resten på stacken (från höger så argumenten ligger vänster->höger på stacken, är ju LIFO).

Det var ett exempel på att det går att göra en kompilator som följer standarden, och hanterar stdarg, utan att den gör på det sätt du säger att alla kompilatorer måste göra. Jag sade aldrig att det var en bra lösning, men du kan inte avfärda exemplet bara för att det är tillräckligt effektivt. Om du får igenom ditt krav på pekar-casts kommer det göra det omöjligt att välja den här lösningen (vilket för övrigt kanske är en bra ide).

Skrivet av Yoshman:

C++ är inte relevant att jämföra med, där har man aldrig lyckats enats om en binärstandard ens på enskilda plattformar.

Vadå "inte relevant att jämföra med"? C och C++ är två besläktade språk, ingen av språkstandarderna reglerar hur saker skall implementeras. I C-fallet har man med tiden lyckats enas om att göra på ett likartat sätt medan i C++-fallet har kompilatorleverantörerna, av olika skäl, valt olika lösningar och ännu inte uppnått samsyn om vad som är det bästa sättet att göra saker och ting. Varför kan man inte jämföra C och C++? För att C++-kompilatorerna gör saker på olika sätt? Det är väl bara ett bevis för att det finns skäl att tillåta olika implementationer? Vi kan väl förutsätta att kompilatortillverkarna inte med flit väljer en kass lösning utan att de har sett olika fördelar med olika lösningar och valt en modell som de trodde skulle passa för just deras miljö. I C-fallet har de haft över 40 år på sig att hitta en bra lösning som verkar passa de flesta.

Men att de flesta kompilatorer valt att göra på ett likartat sätt är inte ett argument för att språkstandarden skall diktera att alla implementationer måste göra på just det sättet. Hur hade det sett ut om Kernighan och Ritchie gjort det 1978? Hade vi fortfarande kört PDP-11 då? Jag ser det som en poäng att standarden inte styr implementationen. Standarden i dag tillåter att kompilatortillverkaren väljer olika lösningar som passar målmaskinen och det är en frihet du tar bort med kravet på att din cast skall fungera. Du kanske ser det som en bra sak, men det gör inte jag. Ju mer standarden kräver, desto färre plattformar kommer det komma kompilatorer för. Och om man i framtiden skulle komma på mer effektiva lösningar på en del av dagens problem, då skulle en striktare språkstandard kunna förbjuda kompilatorerna från använda de nya teknikerna.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Det är en uppenbar kodstorleksoptimering och den kan dessutom ha positiva effekter på cacheutnyttjandet. Självklart anropar konstruktorn operator new på samma sätt som görs i ditt första steg, men det räcker att man gör det på ett ställe istället för att det behöver göras på alla ställen där du gör new på ett objekt. I din värld är kodstorleken gratis, men så är det inte överallt.

Hur kan det var bättre att komplicera logik i stället för att ha en enkel implementation av kroppen till ctor som körs efter att man kört något av varianterna som allokerar minne på stack, heap eller via custom funktion? Hopp är typiskt långsammare att köra för simplare CPUer än kod som staplar instruktioner på varandra då även den simplaste prefetcher fixar sådant.

Skrivet av Ingetledigtnamn:

Det var ett exempel på att det går att göra en kompilator som följer standarden, och hanterar stdarg, utan att den gör på det sätt du säger att alla kompilatorer måste göra. Jag sade aldrig att det var en bra lösning, men du kan inte avfärda exemplet bara för att det är tillräckligt effektivt. Om du får igenom ditt krav på pekar-casts kommer det göra det omöjligt att välja den här lösningen (vilket för övrigt kanske är en bra ide).

C har funnits i snart 40 år, den grundmodell i stort sett alla använder för hur man skickar argument har varit densamma under majoriteten av den tiden. Om eventuella problem med framtida CPU-designer ska vägas in borde man börja med att ta bort saker som longjmp()/setjmp() och framförallt flera saker som infördes i C11 atomic.h då det är i stort sett hopplöst att göra effektiva implementationer på vissa CPU-arkitekturer.

longjmp()/setjmp() kan kan ifrågasätta (går dock att använda för att göra OS-schedulers utan en rad assembler, men håller sig inte inom vad ANSI-C tillåter då det finns minst ett CPU-arkitektur där det inte fungerar, SPARC), men de som man lagt till i atomic.h är så pass användbart och det fungerar bra på de vanligaste CPU-modellerna så är verkligen på tiden att man lade till detta till ANSI-C!

Att kunna göra den cast av funktioner används som sagt redan på många ställen då det är en form av polymorfism som passar språket C som hand i handsken. Just av den anledningen samt att det i praktiken redan fungerar överallt gör att det borde vara explicit tillåtet i standarden.

Skrivet av Ingetledigtnamn:

Vadå "inte relevant att jämföra med"? C och C++ är två besläktade språk, ingen av språkstandarderna reglerar hur saker skall implementeras. I C-fallet har man med tiden lyckats enas om att göra på ett likartat sätt medan i C++-fallet har kompilatorleverantörerna, av olika skäl, valt olika lösningar och ännu inte uppnått samsyn om vad som är det bästa sättet att göra saker och ting. Varför kan man inte jämföra C och C++? För att C++-kompilatorerna gör saker på olika sätt? Det är väl bara ett bevis för att det finns skäl att tillåta olika implementationer? Vi kan väl förutsätta att kompilatortillverkarna inte med flit väljer en kass lösning utan att de har sett olika fördelar med olika lösningar och valt en modell som de trodde skulle passa för just deras miljö. I C-fallet har de haft över 40 år på sig att hitta en bra lösning som verkar passa de flesta.

Och C++ har haft 30 år på sig, men där valde man redan från start att explicit inte ha ett väldefinierat ABI, vilket gör att du kan inte förutsätta att något du bygger med kompilator A kan länkas med något du bygger med kompilator B. Resultatet är att C++ är i princip värdelöst som system/OS-språk då det inte går att använda för att bygga generella tjänster/bibliotek som bara finns i binärform.

Detta är möjligt med C just därför alla system där det överhuvudtaget är rimligt att mer än en kompilator kan användas har ett ABI. De riktigt små systemen med en enda kompilator har i praktiken också ett ad-hoc ABI, det som dikteras av vad den kompilatorn som faktiskt finns gör.

Och trots alla olika lösning man valt på olika CPU-arkitekturer/OS så har ingen relevant spelare (kan som sagt finnas någon DSP/ASIC/FPGA där det blir problem, men då lär man knappast ha ANSI-C stöd heller) valt en ABI som inte i praktiken stödjer den cast jag föreslog, så vad är problemet?

Skrivet av Ingetledigtnamn:

Men att de flesta kompilatorer valt att göra på ett likartat sätt är inte ett argument för att språkstandarden skall diktera att alla implementationer måste göra på just det sättet. Hur hade det sett ut om Kernighan och Ritchie gjort det 1978? Hade vi fortfarande kört PDP-11 då? Jag ser det som en poäng att standarden inte styr implementationen. Standarden i dag tillåter att kompilatortillverkaren väljer olika lösningar som passar målmaskinen och det är en frihet du tar bort med kravet på att din cast skall fungera. Du kanske ser det som en bra sak, men det gör inte jag. Ju mer standarden kräver, desto färre plattformar kommer det komma kompilatorer för. Och om man i framtiden skulle komma på mer effektiva lösningar på en del av dagens problem, då skulle en striktare språkstandard kunna förbjuda kompilatorerna från använda de nya teknikerna.

Rätt säker på att den ABI man använder idag på PDP-11 är identisk med den man använde på 70-talet. Den cast jag gjorde gör inte någon inskränkning i vilka CPU-arkitekturer man kan stödja, om argument skickas på stacken, register eller via globalt minne. Den enda inskränkningen (som idag ändå görs av ABI) skulle vara att man måste ha en väldefinierad ordning för hur argument matchas med register/plats på stacken. Detta gäller endast för funktioner med extern länkning eller de med intern länkning som man tar adressen av, fast det är också något som dagens kompilatorer redan måste ta hänsyn till av andra skäl.

Skulle säga tvärt om vad det gäller värdet att en standard dikterar även saker kring implementation. Detta görs t.ex. i båda Java/JVM och C#/CLR, fördelen med detta är uppenbart när man jämför med den röra det blev med C++. I C har man inte fått problem därför att det i praktiken alltid finns en binärstandard på varje system/CPU.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Hur kan det var bättre att komplicera logik i stället för att ha en enkel implementation av kroppen till ctor som körs efter att man kört något av varianterna som allokerar minne på stack, heap eller via custom funktion? Hopp är typiskt långsammare att köra för simplare CPUer än kod som staplar instruktioner på varandra då även den simplaste prefetcher fixar sådant.

Kodstorlek var under en lång det i stort sett det enda optimeringsmålet man hade i embedded-världen. Om man lyckas krympa koden så den ryms i ett mindre prom eller mindre on-chip-minne blir produkten billigare och det märks i kassakistan. Det är oftast mycket viktigare än att koden går fort.

Skrivet av Yoshman:

Detta är möjligt med C just därför alla system där det överhuvudtaget är rimligt att mer än en kompilator kan användas har ett ABI. De riktigt små systemen med en enda kompilator har i praktiken också ett ad-hoc ABI, det som dikteras av vad den kompilatorn som faktiskt finns gör.

Och trots alla olika lösning man valt på olika CPU-arkitekturer/OS så har ingen relevant spelare (kan som sagt finnas någon DSP/ASIC/FPGA där det blir problem, men då lär man knappast ha ANSI-C stöd heller) valt en ABI som inte i praktiken stödjer den cast jag föreslog, så vad är problemet?

Som du skriver har olika C-kompilatorer valt att använda samma ABI. Det är ett val som kompilatortillverkarna har gjort och inget som standarden dikterar. Mitt problem består i att du vill att standarden skall bestämma hur ABIt ser ut.

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Kodstorlek var under en lång det i stort sett det enda optimeringsmålet man hade i embedded-världen. Om man lyckas krympa koden så den ryms i ett mindre prom eller mindre on-chip-minne blir produkten billigare och det märks i kassakistan. Det är oftast mycket viktigare än att koden går fort.

Men i detta fall lär en separation av funktionerna inte bara vara snabbare, det lär vara mindre också då allokeringsdelen kan återanvändas till alla klasser av en viss storlek medan ctor kroppen blir mindre om den bara behöver bry sig om initiering.

Skrivet av Ingetledigtnamn:

Som du skriver har olika C-kompilatorer valt att använda samma ABI. Det är ett val som kompilatortillverkarna har gjort och inget som standarden dikterar. Mitt problem består i att du vill att standarden skall bestämma hur ABIt ser ut.

Skulle mer säga att det är ett rätt påtvingat val. Följer man inte ABI kommer ingen använda kompilatorn för det går inte att använda redan existerande bibliotek som kommer i binärform. Just avsaknaden av binärstandard för C++ har gjort att man valt bort detta språk för flera saker.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Men i detta fall lär en separation av funktionerna inte bara vara snabbare, det lär vara mindre också då allokeringsdelen kan återanvändas till alla klasser av en viss storlek medan ctor kroppen blir mindre om den bara behöver bry sig om initiering.

Om man flyttar operator new-anropet (villkorat) till ctor kan ett anrop till operator new + ett anrop till ctor reduceras till bara ett anrop till ctor på varje ställe i koden där du gör new. Gör du new på samma klass på N ställen kan man ta bort N anrop till operator new, istället får du 1 anrop i ctor. Vinst N - 1 färre funktionsanrop (statiskt). Hur menar du att man skulle kunna återanvända ett operator new-anrop till ett annat objekt av samma storlek?

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Om man flyttar operator new-anropet (villkorat) till ctor kan ett anrop till operator new + ett anrop till ctor reduceras till bara ett anrop till ctor på varje ställe i koden där du gör new. Gör du new på samma klass på N ställen kan man ta bort N anrop till operator new, istället får du 1 anrop i ctor. Vinst N - 1 färre funktionsanrop (statiskt). Hur menar du att man skulle kunna återanvända ett operator new-anrop till ett annat objekt av samma storlek?

Till att börja med ska du hantera tre separata fall då minnet kan komma från stacken (automatisk variabel), heapen (default new) eller custom new. Där har du 2 villkor med tillhörande hopp för en gren.

Om du i stället separerar minnesallokering så blir fallet med automatisk variabel
1. addera sizeof(din_typ) till stacken, det är ditt utrymme för objektet
2. toppen på stacken är nu this, så lägg den där första argumentet ska vara
3. anropa ctor (som du ändå måste göra)

För heap/custom
1. spara storleken på din typ som argument
2. spara adressen till ctor som argument (heap allokeraren gör ett indirekt anrop till denna så den kan då hantera alla ctors)

alla dina ctors kan nu helt skita i minneshantering då de får minnet som första argument ("this"). Och det görs bara ett anrop där du skapar objektet oavsett vart minnet kommer ifrån.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Till att börja med ska du hantera tre separata fall då minnet kan komma från stacken (automatisk variabel), heapen (default new) eller custom new. Där har du 2 villkor med tillhörande hopp för en gren.

Om du i stället separerar minnesallokering så blir fallet med automatisk variabel
1. addera sizeof(din_typ) till stacken, det är ditt utrymme för objektet
2. toppen på stacken är nu this, så lägg den där första argumentet ska vara
3. anropa ctor (som du ändå måste göra)

För heap/custom
1. spara storleken på din typ som argument
2. spara adressen till ctor som argument (heap allokeraren gör ett indirekt anrop till denna så den kan då hantera alla ctors)

alla dina ctors kan nu helt skita i minneshantering då de får minnet som första argument ("this"). Och det görs bara ett anrop där du skapar objektet oavsett vart minnet kommer ifrån.

Man har inte både default operator new och klasspecific operator new. Man har normalt antingen eller. Ja, det går att tvinga fram fall där man kan köra båda, men man optimerar för normalfallet.

Ctor tar givetvis en this-pekare. Den pekar ut minnet där objektet skall konstrueras om det redan finns minne allokerat (exempelvis för auto eller om man absolut vill anropa default operator new trots and man deklarerat en egen i klassen). Om flaggan säger att man skall allokera minne anropas operator new (klasspecifik om det finns annars ::operator new) och this tilldelas resultatet. Sedan kör man resten av konstruktorn precis som vanligt.

När du vill skicka ctor-adressen till heap-allokeraren försöker du göra samma trick och trolla bort ett av anropen, fast åt andra hållet så att säga. Hur hade du tänkt det skulle fungera för andra ctor:er än default-ctor:n?

Permalänk
Datavetare
Skrivet av Ingetledigtnamn:

Man har inte både default operator new och klasspecific operator new. Man har normalt antingen eller. Ja, det går att tvinga fram fall där man kan köra båda, men man optimerar för normalfallet.

Ctor tar givetvis en this-pekare. Den pekar ut minnet där objektet skall konstrueras om det redan finns minne allokerat (exempelvis för auto eller om man absolut vill anropa default operator new trots and man deklarerat en egen i klassen). Om flaggan säger att man skall allokera minne anropas operator new (klasspecifik om det finns annars ::operator new) och this tilldelas resultatet. Sedan kör man resten av konstruktorn precis som vanligt.

När du vill skicka ctor-adressen till heap-allokeraren försöker du göra samma trick och trolla bort ett av anropen, fast åt andra hållet så att säga. Hur hade du tänkt det skulle fungera för andra ctor:er än default-ctor:n?

Sant att man endera har custom new eller "vanliga" new per typ.

Angående argumenten, inser att det finns ett ännu enklare sätt att separera allokering och ctor som även gör så att argumenten trillar på plats helt automatiskt.

  1. allokera minne endera på stacken, heapen eller custon new (som måste returnera pekare till utrymmet enligt C++ språkstandard), lägg adressen till minnet där första argumentet ska vara (this)

  2. stoppa dit alla argument som ctor tar

  3. anropa ctor, this pekar nu på objektutrymmet och resten av argument är där de förväntas vara

I detta läge kan man ha samma minnesallokeringslogik för alla typer oavsett antal argument till ctor och ctor behöver överhuvudtaget inte bry sig om hur minnet allokeras.

Beskrivning du gör är typ omvänd Pascal-calling och det blir problem vid automatiska variabler då det är anropad funktion som allokerar utrymmet på stacken -> helt omöjligt att hoppa tillbaka till den som anropar på "normalt" sätt då returadress normalt ligger på stacken och den typen av instruktioner modifierar stackpekare (vars topp nu är utrymmet för this!). Även om man kommer tillbaka har nu den som anropar ett problem, den måste veta hur mycket minne ctor allokerade annars går det inte att fria minnet på korrekt sätt -> extra tillstånd som tar instruktioner och utrymme någonstans.

Visa signatur

Care About Your Craft: Why spend your life developing software unless you care about doing it well? - The Pragmatic Programmer

Permalänk
Skrivet av Yoshman:

Rätt säker på att den ABI man använder idag på PDP-11 är identisk med den man använde på 70-talet. Den cast jag gjorde gör inte någon inskränkning i vilka CPU-arkitekturer man kan stödja, om argument skickas på stacken, register eller via globalt minne. Den enda inskränkningen (som idag ändå görs av ABI) skulle vara att man måste ha en väldefinierad ordning för hur argument matchas med register/plats på stacken. Detta gäller endast för funktioner med extern länkning eller de med intern länkning som man tar adressen av, fast det är också något som dagens kompilatorer redan måste ta hänsyn till av andra skäl.

Jag betvivlar inte att en kompilator för PDP-11 genererar kod på snarlikt sätt som man gjorde på 70-talet. Vad jag undrade var snarare om skulle det kunna ha haft negativa effekter om man i standarden bestämt att det ABI som då användes var rättesnöret som alla andra kompilatorer skulle följa. Det vore kanske inte så bra om alla kompilatorer var tvungna att hantera flyttalen på samma sätt som man gjorde då.