Returnera temporärt "delobjekt" (C++)

Permalänk
Medlem

Returnera temporärt "delobjekt" (C++)

Säg att jag har en klass A som håller koll på n antal listor.
Denna klass har ett antal medlemsfunktioner op1(), op2(), ... etc, som vardera utför en specifik operation på ALLA listor.

Säg nu att jag skulle vilja utföra en operation på bara EN lista. En lösning skulle såklart vara att lägga till ett argument till alla funktioner som anger vilken lista jag vill operera på. Men jag undrar om det inte går att göra på ett smidigare och snyggare sätt?

Tanken om syntax och funktion jag har är följande, om jag vill operera på alla listor utför jag:

Aobj.op1( ... );

Men om jag bara vill utföra operationen på en lista (säg 3:e i ordningen) vill jag kunna skriva exempelvis:

Aobj[2].op1( ... );

eller något i den stilen.
Dessa listor är inte nödvändigtvis en simpel array, utan kan vara vadsomhelst.

Iden som jag lekt med är att med operator[] returnera en referens till en temporär instans av typen A, där den temporära instansen innehåller relevanta pekare och dimensionsvariabler till den delmängd av datan jag vill operera på. Den temporära instansen lagras som en medlemsvariabel i objektet själv som en pekare. Det fungerar, men är inte så snyggt, genererar en hel del "bookkeeping", och är inte trådsäkert (men behöver inte heller vara i nuläget).

Någon som har kännedom om något effektivt (dvs ingen deep copy) och beprövat sätt att göra detta? Eller någon länk till relevant läsning?

Permalänk
Datavetare

Om din "klass A" har listorna som medlemsvariabler via std::shared_ptr<ListType> så blir det enkelt och effektivt att skapa en temporär klass A som bara innehåller den listan du vill jobba med. std::shared_ptr<> är "trådsäker" i bemärkelsen att referensräkningen fungerar korrekt även i multitrådade program.

Har bara tittat på implementation av std::shared_ptr<> i detalj för g++ på Linux, men där är den riktigt elegant i att om man inte länkar med libpthread så sköts referensräkningen via vanliga ++/-- operationer som är väldigt billiga. Länkar man däremot mot libpthread så sköts referensräkningen via atomära variabler som är betydligt dyrare men ger ett korrekt resultat även om flera trådar jobbar mot samma std::shared_ptr<> instans.

std::shared_ptr<> är del av standardbiblioteket i C++11. Om du inte har en C++11 kompilator kan du även hitta std::shared_ptr i boost.

Notera att du fortfarande måste se till att accessen till varje lista är multitrådsäkert om du kommer köra i en sådan miljö, std::shared_ptr<> garanterar bara att livslängden på objektet är korrekt i en sådan miljö.

Fördelen med std::shared_ptr<> över den lösning du skriver om ovan är du slipper all "book-keeping" då det sköts via std::shared_ptr<>

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

Jag mår lite dåligt varje gång någon skriver "trådsäkert". Anledningen är att de ofta menar att du kan spawna upp två trådar som kan göra precis vad som helst och det ändå fungerar. I 99% av fallen är detta en väldigt dålig idé. Det är mycket bättre att ha en main-tråd som skickar jobb till andra trådar i form av en datamängd och det den behöver för att köras. Om dina trådar kräver massa synkronisering kommer det garanterat bli långsammare än att köra allt på en tråd. Och om du använder shared_ptr:s och känner ett behov av att de ska vara trådsäkra är du ute på hal is.

Permalänk
Medlem
Skrivet av Yoshman:

Om din "klass A" har listorna som medlemsvariabler via std::shared_ptr<ListType> så blir det enkelt och effektivt att skapa en temporär klass A som bara innehåller den listan du vill jobba med. std::shared_ptr<> är "trådsäker" i bemärkelsen att referensräkningen fungerar korrekt även i multitrådade program.

Har bara tittat på implementation av std::shared_ptr<> i detalj för g++ på Linux, men där är den riktigt elegant i att om man inte länkar med libpthread så sköts referensräkningen via vanliga ++/-- operationer som är väldigt billiga. Länkar man däremot mot libpthread så sköts referensräkningen via atomära variabler som är betydligt dyrare men ger ett korrekt resultat även om flera trådar jobbar mot samma std::shared_ptr<> instans.

std::shared_ptr<> är del av standardbiblioteket i C++11. Om du inte har en C++11 kompilator kan du även hitta std::shared_ptr i boost.

Notera att du fortfarande måste se till att accessen till varje lista är multitrådsäkert om du kommer köra i en sådan miljö, std::shared_ptr<> garanterar bara att livslängden på objektet är korrekt i en sådan miljö.

Fördelen med std::shared_ptr<> över den lösning du skriver om ovan är du slipper all "book-keeping" då det sköts via std::shared_ptr<>

Har inte använt std::shared_ptr innan, men det låter som det kan göra vad jag vill! Jobbet som ska utföras på varje lista skickas vidare som ett paket till annan instans (GPU), så det är multitrådsäkert i den meningen. Ska läsa på lite. Tack för tipset!

Permalänk
Medlem
Skrivet av grovlimpa:

Jag mår lite dåligt varje gång någon skriver "trådsäkert". Anledningen är att de ofta menar att du kan spawna upp två trådar som kan göra precis vad som helst och det ändå fungerar. I 99% av fallen är detta en väldigt dålig idé. Det är mycket bättre att ha en main-tråd som skickar jobb till andra trådar i form av en datamängd och det den behöver för att köras. Om dina trådar kräver massa synkronisering kommer det garanterat bli långsammare än att köra allt på en tråd. Och om du använder shared_ptr:s och känner ett behov av att de ska vara trådsäkra är du ute på hal is.

Vad tråkigt att jag fick dig att må dåligt, helt i onödan Vad jag menade med att min första testmetod inte var "trådsäkert" var att om tråd 1 får en referens till lista 1, och tråd 2 för en referens till lista 2 strax efteråt, så kommer tråd 2 "förstöra" instansen (ändra pekarna) till lista 1 medan tråd 1 jobbar med den. Givetvis kommer jag inte sätta ett godtyckligt antal CPU-trådar till att utföra lista 1 hursomhelt, blir kallsvettig bara jag tänker tanken hur det skulle gå till. Bara en CPU-tråd kommer hantera en given lista.

Permalänk
Datavetare

@grovlimpa

"Trådsäkert" är egentligen alldeles för oprecist och tar man det bokstavligt när det man delar är "mutable" så blir det typiskt väldigt mycket lås.

Bjarne Stroustrup (skapare av C++, men gissar att alla redan visste det) har i flera av sina tal pekat på att användandet av std::share_ptr<> är nästa alltid en indikation på att den överliggande designen är fel, man bör normalt sett alltid klara sig med att skicka saker by-value (något som är betydligt mer effektivt i C++11 tack vare move-semanics eller med std::unique_ptr<>. Ingen av dessa fungerar givet beskrivningen trådskaparen ger.

I detta fall var kravet att det inte skulle finnas "deep-copies" (så by-value är uteslutet) och kravet var också att den ursprungliga listan ska vara kvar (så move-semantics är uteslutet). Bästa sättet att undvika "deep-copies" men ändå vara "trådsäker" är att använda sig av s.k. persistent-data-structures, tyvärr krävs det att man har ett miljö med GC för att effektivt kunna använda persistenta datastrukturer vilket C++ saknar i standardformat (men det finns i språk som Clojure, Scala, Haskell och Erlang).

Håller med att om att rent generellt så är delad data och multitråding en väldigt dålig idé, men man måste skilja på concurrent programming och parallel programming.

Concurrent programming är hur man effektivt och korrekt hanterar system som innehåller flera kontext som har sin egen programräknare och stack.
Parallel programming är hur man effektivt och korrekt skriver program som kör på flera CPU-kärnor. Alla parallella program är "concurrent", men det omvända är inte sant.

I "concurrent programming" så kan det vara helt OK att använda sig av lås för att skydda tillstånd (men oftast är det bättre att göra en kopia av allt data), målet här är väldigt sällan maximal parallellism och problemen man löser är väldigt ofta begränsade av I/O och inte av CPU-kraft. Väldigt mycket programmering på servers och liknande hamnar i denna kategori. Det mål man ofta har är att maximera antal I/O operationer system kan utföra. C++ är kanske inte världens bästa val för "concurrent programming", språk som Clojure, Scala och framförallt Erlang och Go (som båda bygger på CSP (Communicating Sequential Processes) är betydligt bättre val.

I "parallel programming", något jag jobbat med under ett par år nu, så är målet att använda en given HW så effektiv det bara går. I detta fall får man ibland gå ifrån "best practices" i hur man bör konstruera program då de mest prestandakritiska delarna måste vara skrivna så de är så "snäll" mot underliggande HW som möjligt. I dessa fall är det inte alls omöjligt att man väljer att dela "mutable" state (men det hör till ovanligheten då kopiering av data är mycket billigare än vad många tror på moderna CPUer), också väldigt vanligt att man måste veta hur något är implementerat vilket är anledningen varför jag läst igenom en stor del av implementationen av g++/STL på Linux. C och/eller C++ är ofta nödvändigt ont för effektiv "parallel programming".

Men detta är väldigt OT då trådskaparen skriver att trådsäkerhet inte ens är relevant för tillfället, själv ville jag bara peka på att std::shared_ptr<> faktiskt hanterar en multitrådad miljö, men bara för att avgöra när en viss instans ska destrueras.

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
Skrivet av Mikael07:

Säg att jag har en klass A som håller koll på n antal listor.
Denna klass har ett antal medlemsfunktioner op1(), op2(), ... etc, som vardera utför en specifik operation på ALLA listor.

Säg nu att jag skulle vilja utföra en operation på bara EN lista. En lösning skulle såklart vara att lägga till ett argument till alla funktioner som anger vilken lista jag vill operera på. Men jag undrar om det inte går att göra på ett smidigare och snyggare sätt?

Tanken om syntax och funktion jag har är följande, om jag vill operera på alla listor utför jag:

Aobj.op1( ... );

Men om jag bara vill utföra operationen på en lista (säg 3:e i ordningen) vill jag kunna skriva exempelvis:

Aobj[2].op1( ... );

eller något i den stilen.
Dessa listor är inte nödvändigtvis en simpel array, utan kan vara vadsomhelst.

Iden som jag lekt med är att med operator[] returnera en referens till en temporär instans av typen A, där den temporära instansen innehåller relevanta pekare och dimensionsvariabler till den delmängd av datan jag vill operera på. Den temporära instansen lagras som en medlemsvariabel i objektet själv som en pekare. Det fungerar, men är inte så snyggt, genererar en hel del "bookkeeping", och är inte trådsäkert (men behöver inte heller vara i nuläget).

Någon som har kännedom om något effektivt (dvs ingen deep copy) och beprövat sätt att göra detta? Eller någon länk till relevant läsning?

Du skulle möjligtvis kunna ändra lite på din klasstruktur. Istället för att ha ett objekt av klass A med godtyckligt antal listor har du godtyckligt antal instanser av klass A som vardera har en lista. Du wrappar godtyckligt antal objekt av klass A i ett objekt ACluster, som implementerar samma interface som klass A men delegerar det inkommande anropet till vardera instans.

För att hantera en specifik lista — eller för att vara mer specifik en instans av A som wrappats av ACluster — överlagrar du [] på ACluster. På detta vis är listdatan fortfarande oåtkomlig utifrån och bara objektet A:s metoder kan köras på det. Du slipper därtill skapa ett temporärt objekt.

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Medlem
Skrivet av Mikael07:

Vad tråkigt att jag fick dig att må dåligt, helt i onödan Vad jag menade med att min första testmetod inte var "trådsäkert" var att om tråd 1 får en referens till lista 1, och tråd 2 för en referens till lista 2 strax efteråt, så kommer tråd 2 "förstöra" instansen (ändra pekarna) till lista 1 medan tråd 1 jobbar med den. Givetvis kommer jag inte sätta ett godtyckligt antal CPU-trådar till att utföra lista 1 hursomhelt, blir kallsvettig bara jag tänker tanken hur det skulle gå till. Bara en CPU-tråd kommer hantera en given lista.

Det är inte ditt fel, det är ett pathfinding-middleware för några tiotusentals USD som fick mig att må dåligt när jag hör det ordet

För att gå on-topic igen håller jag med om att det kan vara bra att separera data och instanser som pekar på datan. Qt använder det väldigt mycket, dvs att en array/vector-klass pekar på ett referensräknat minnesblock (eller en delmängd av det). Fördelen är att om du inte kör C++11 så får du typ move-semantics och du använder mindre minne om flera instanser innehåller samma data. Qt kopierar dock datan när du ändrar den (om jag inte minns fel), vilken du inte vill göra.

Permalänk
Datavetare
Skrivet av Teknocide:

Du skulle möjligtvis kunna ändra lite på din klasstruktur. Istället för att ha ett objekt av klass A med godtyckligt antal listor har du godtyckligt antal instanser av klass A som vardera har en lista. Du wrappar godtyckligt antal objekt av klass A i ett objekt ACluster, som implementerar samma interface som klass A men delegerar det inkommande anropet till vardera instans.

För att hantera en specifik lista — eller för att vara mer specifik en instans av A som wrappats av ACluster — överlagrar du [] på ACluster. På detta vis är listdatan fortfarande oåtkomlig utifrån och bara objektet A:s metoder kan köras på det. Du slipper därtill skapa ett temporärt objekt.

Det är definitivt en riktigt bra lösning, i alla språk som hanterar livstiden på objekt via en GC... I C++ kan det leda till dangling-pointers och/eller referenser till objekt som inte längre är giltiga. operator[] måste returnera en pekare eller referens för att undvika en "deep-copy" men utan att returnera temporär-objekt av något slag (t.ex. en shared_ptr<> eller någon annan form av temporärt proxy-objekt) så finns det inget som garanterar att den referens / pekare man returnerar inte används efter objektet som refereras / pekas på destruerats.

Å andra sidan är detta C++, så man ska kunna göra saker som kan resultera i att man skjuter av sig hela benet bara man som programmerare anser att det är en lämplig design
Det går däremot på tvärs mot hur man i C++11 faktiskt försöker göra C++ ett mer modernt språk utan dessa gropar.

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
Skrivet av Yoshman:

Det är definitivt en riktigt bra lösning, i alla språk som hanterar livstiden på objekt via en GC... I C++ kan det leda till dangling-pointers och/eller referenser till objekt som inte längre är giltiga. operator[] måste returnera en pekare eller referens för att undvika en "deep-copy" men utan att returnera temporär-objekt av något slag (t.ex. en shared_ptr<> eller någon annan form av temporärt proxy-objekt) så finns det inget som garanterar att den referens / pekare man returnerar inte används efter objektet som refereras / pekas på destruerats.

Å andra sidan är detta C++, så man ska kunna göra saker som kan resultera i att man skjuter av sig hela benet bara man som programmerare anser att det är en lämplig design
Det går däremot på tvärs mot hur man i C++11 faktiskt försöker göra C++ ett mer modernt språk utan dessa gropar.

Jag förstår. Innerbär inte det att man då praktiskt taget aldrig kan returnera en referens av ett objekt vars existens beror på ett annat objekt som kan komma att bli de[kon?]struerat? Det låter ju oerhört... dumt, ur ett objektorienteringsperspektiv.

Man kanske då istället kan returnera en shared_ptr<A> som pekar på den önskade instans av A som ACluster håller? Jag gissar bara här, har aldrig rört C++, men det är ganska intressant att få reda på

Visa signatur

Kom-pa-TI-bilitet

Permalänk
Datavetare
Skrivet av Teknocide:

Jag förstår. Innerbär inte det att man då praktiskt taget aldrig kan returnera en referens av ett objekt vars existens beror på ett annat objekt som kan komma att bli de[kon?]struerat? Det låter ju oerhört... dumt, ur ett objektorienteringsperspektiv.

Man kanske då istället kan returnera en shared_ptr<A> som pekar på den önskade instans av A som ACluster håller? Jag gissar bara här, har aldrig rört C++, men det är ganska intressant att få reda på

Det skulle fungera, "tricket" i C++ för att få "automatisk" minneshantering är att du vill använda RAII (Resource Acquisition Is Initialization) genom att lägga något form av proxyobjet på stacken där du "allokerar" i ctor och "deallokerar" i dtor. Ett sådant "proxyobjekt" är shared_ptr<> där en eller fler sådan objekt pekar på samma underliggande objekt, ctor av varje nytt shared_ptr<> till ett givet objekt räknar upp referensen och dtor räknar ner, när sista referensen försvinner anropas "delete" på underliggande objektet.

Naturligtvis kvarstår de vanliga problemen med referensräkning: cykliska strukturer kommer leda till minnesläcka om man inte explicit hanterar dessa. Så nog finns det stora fördelar med "tracing GC".

Men då anrop av dtor i C++ är deterministiskt så finns det en hel del trevliga saker man kan göra via RAII som förenklar koden, det du får via C# "using(temprary_object)" är ju extremt enkelt i C++ och kräver inga nya nyckelord. Ett missat "using" kan ju leda till en resursläcka då man inte vet när "Dispose" körs på objektet, är resursen i fråga något form av lås så kan detta i sin tur leda till dead-lock. Andra fördelar med referensräkning är att livslängen på objekt är helt deterministisk, vilket ofta är att föredra i interaktiva applikationer då man inte kommer få oväntade pauser i vissa fall / på vissa system. Tiden för att destruera objekt är också utspridd i tiden vilket ger bättre genomsnittlig latens, men GC har fördelen att destruktion av objekt sker i klumpar vilket är dåligt för latens men bra för att maximera totala antalet operationer per sekund (vilket man ofta vill på t.ex. servers).

Många anser också att "concurrent programming" utan GC leder inte helt sällan till kniviga situationer i att hantera livslängd på data korrekt och effektivt. Kan bara hålla med detta. Har tittat en del på Go som från grunder är ett språk designat just för "concurrent programming" (bl.a. med "tracing GC" och språkstöd för att säkert/korrekt/effektiv kommunicera mellan samtidigt körande kontext) och det är mycket enklare att göra sådana program i Go jämfört med t.ex. C++, C#, Java eller C (kör C på jobbet p.g.a. då det är ett krav där våra saker används).

Visa signatur

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