🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

Article View: pl.comp.lang.c
Article #487

Wskaźniki tylko do odczytu (Re: C.D. Segmentation fault przy strtok...)

#487
From: Qrczak
Date: Sat, 10 Jan 1998 11:05
287 lines
16748 bytes
Followup-To: pl.comp.programming, bo przechodzę do ogólnych rozważań
niezależnych od języka.

On Thu, 8 Jan 1998, Marek Kotowski wrote:

> > Pretensje kierować do autorów C, że w ogóle pozwolili używać funkcji bez
> > prototypów.
> 
> Pozwolili, bo inaczej uczyniliby wiele istniejacych programow
> formalnie niepoprawnymi.

Najpierw powstał język, a potem programy w nim napisane. Trzeba go było
robić z głową, a nie tak, żeby później dla kompatybilności trzeba było
zadhowywać kiepskie pomysły.

> > Więc nie można tak wywoływać, bo wartości *linia nie można zmieniać! W tym
> > przypadku lepiej zrobić deklarację linia[] = "la la la";, co przy okazji
> > trochę przyspieszy kod (bo wskaźnik linia będzie stałą, a nie zmienną;
> > linia[3] to będzie ustalone miejsce w pamięci, a nie wartość zmiennej
> > wskaźnikowej z dodaną trójką).
> 
> Watpie. W obydwu przypadkach strtok zwraca adres. Poza tym notacja
> indeksowa jest zawsze zamieniana na wskaznikowa. Wreszcie zycie uczy,
> ze jakiekolwiek wyrokowanie o wyzszej efektywnosci notacji
> wskaznikowej nad indeksowa lub odwrotnie jest ryzykowne. Wszystko
> zalezy od kompilatora.

To nie jest znana kwestia wskaźniki kontra indeksy.

Załóżmy na chwilę dla uproszczenia, że linia jest zmienną statyczną.

Jeśli teraz linia jest typu char *, to w pamięci (ewentualnie w
rejestrze procesora) mamy zmienną zawierającą wskaźnik do właściwego
łańcucha. Odczytanie 3ego znaku tego łańcucha wymaga wzięcia tej
wartości z pamięci albo z rejestru i dodanie trójki.

Jeśli natomiast linia jest zwykłą tablicą znaków, to jej adres jest
stały. Odczytanie 3ego znaku tego łańcucha to wzięcie bajtu spod
konkretnego adresu w pamięci! Nigdy nie będzie to wolniejsze od
odczytania najpierw adresu początku łańcucha, a potem bajtu spod
obliczonego adresu.

Jeśli teraz linia jest zwykłą zmienną lokalną, to jest podobnie, tylko
jakby jeden poziom dalej. W przypadku wskaźnika do tablicy najpierw
trzeba odczytać ze stosu adres, a potem spod tego adresu zwiększonego o
3 odczytać znak. A w przypadku tablicy umieszczonej bezpośrednio na
stosie od razu odczytujemy odpowiedni bajt, tak samo szybko jak
poprzednio odczytaliśmy adres. Tyle że gorzej wtedy z zainicjowaniem tej
wartości - łatwiej zainicjować wskaźnik niż całą wartość, którą trzeba
przepisać. Dlatego jeśli to jest stała, która nie będzie się zmieniać,
to powinna być statyczna (ewentualnie wskaźnik). Ale jeśli będzie
argumentem strtok, to wręcz musi być bezpośrednio tablicą, a nie
wskaźnikiem do tablicy, żeby strtok mogło zmieniać zawartość tego
napisu! I będzie przepisywana przy każdym wywołaniu danej funkcji. Tak
ma być.

> > Pretensje kierować do autorów C, że stałe łańcuchowe są typu char *, a nie
> > const char *, tzn. że nie wymuszają używania const.
> 
> Wytlumacze ich. Nie przyjeli kwalifikatora const dla literalow
> znakowych, bo w ten sposob ograniczaliby mozliwosc ich stosowania jako
> argumentow w wywolaniach funkcji, takze standardowych, ktore maja
> argumenty bez const (kompilatory moglyby nie akceptowac takich
> wywolan).

To dlaczego te standardowe funkcje nie mają tego consta? IMHO tylko
dlatego, że tak było dawniej. Kiedyś nie było tego consta i teraz już
nie można go wszędzie wymuszać, jeśli chcemy zachować kompatybilność.
Nie pierwszy to raz pokutują błędy przeszłości i dla zachowania
kompatybilności coś musi być gorsze niż mogłoby być, gdyby to wymyślać
od początku.

> Prawda jest natomiast, ze postuluje sie, by w nowym standardzie jezyka
> C literaly znakowe mialy atrybut const, a nawet dwa:
> 
> const char * const p = "Tekst";

Tak zawsze można napisać i dobrze. Kłopot w tym, że teraz równie dobrze
można pominąć drugie const i też się skompiluje, nawet bez ostrzeżenia,
a jedyna różnica (pomijając stary kod, który nie używał consta i wymagał
czegoś takiego) będzie taka, że będą dozwolone dodatkowe bezsensowne
konstrukcje (które zmieniają zawartość stałej).

> Cokolwiek zostanie przyjete, nie bedzie to mialo charakteru
> obligatoryjnego. W przeciwnym razie duza czesc istniejacego
> oprogramowania stalaby sie bledna, co byloby sprzeczne z podstawowymi
> zalozeniami tworzenia standardu.

Zgadza się. Po prostu nienajlepszy jest ten standard.

Tak naprawdę to samo const nie jest wystarczające, żeby to było wygodne
bez ręcznej konwersji wskaźników constowych na zwykłe w niektórych
sytuacjach. Mianowicie można sobie wyobrazić funkcję, która działa jak
indeksowanie łańcuchów i zwraca referencje do znaków. Teraz jeśli jest
wywołana z argumentem typu char *, to powinna zwrócić char &, a jeśli
argumentem jest const char *, to powinna zwrócić const char &. Tak
działa zwykłe wbudowane indeksowanie tablic! Jeśli będzie zdefiniowana
tylko jedna z tych wersji, to w przypadku użycia w tym drugim znaczeniu
bez jawnej konwersji typu - kompilator będzie ostrzegał o zgubieniu
consta (raz przy przekazywaniu argumentu do tej funkcji, a raz przy
używaniu jej wyniku, w zależności od tego, na którą wersję się
zdecydujemy). Można oczywiście zdefiniować obie wersje, ale będą miały
ten sam kod, nawet skompilowany będzie zdublowany! Chyba że mamy
genialny kompilator.

Żeby nie powtarzać tego samego kodu, można zdefiniować normalnie tylko
char &indeks (char *s, int i), a wersję constową wyprodukować tak:

inline const char &indeks (const char *s, int i)
{
  return indeks ((char *) s, i);
}

Konwersja ta jest typu "formalnie niebezpiecznej, chociaż w tym
przypadku działającej poprawnie" - bo skądinąd wiem, że właściwa funkcja
indeks nie zmienia zawartości wskaźnika, mimo że jest on zadeklarowany
bez consta.

Albo odwrotnie: można zdefiniować normalnie tylko
const char &indeks (const char *s, int i), a wersję bez consta
wyprodukować tak:

inline char &indeks (char *s, int i)
{
  return (char &) indeks ((const char *) s, i);
}

Konwersja argumentu s jest tym razem w pełni bezpieczna i służy tylko do
tego, żeby została użyta odpowiednia funkcja. To konwersja wyniku jest
teraz "formalnie niebezpieczna" - ale skądinąd wiem, że właściwa funkcja
indeks była akurat wywołana z argumentem, który naprawdę można zmieniać,
mimo że jest typu z constem, więc w tym przypadku znowu wszystko będzie
działać.

Oba rozwiązania wydają mi się "nieładne". Nie dość, że ich kod jest
"formalnie niebezpieczny", to jeszcze niewygodnie się takie rzeczy
pisze.

IMHO dobre rozwiązanie tego problemu powinno wyglądać tak, że oprócz
typów w wersji bez consta i wersji z constem, byłaby wersja z "być może
constem", która miałaby sens tylko jeśli definiujemy funkcję, która ma
argument takiego typu i wynik takiego typu. Czyli jeśli definiuję
funkcję:

maybeconst char &indeks (maybeconst char *s, int i)
{
  return s[i];
}

to znaczy, że w rzeczywistości definiuję dwie funkcje: jedną z constami
w obydwów miejscach i drugą bez constów. Obie dzielą się wspólnym kodem,
bo tak się składa, że te same operacje na wartościach z constem i bez
consta różnią się _tylko_ "formalnym typem" danych i wyniku, a w kodzie
maszynowym zapisują się tak samo. Różnią się tylko tym, że na etapie
kompilacji w pewnych miejscach nie można użyć jednych wartości, a w
innych miejscach - drugich.

U mnie, w moim języku, ten const będzie prawdopodobnie tylko przy
wskaźnikach (a tak naprawdę to przy referencjach - u mnie będą prawie
same referencje). To znaczy będą dwa podstawowe rodzaje wskaźników:
zapisywalne i tylko do odczytu. Pierwsze odpowiadają typowi typ *, a
drugie - const typ *. Dodatkowo będzie trzeci rodzaj, który służy tylko
do takiego jak powyższe skrótowego definiowania funkcji, które jakby
tylko przekazują constowość wskaźnika dalej, same w nią nie zaglądając i
działając niezależnie od niej.

Chyba jedyne wady takiego rozwiązania to tworzenie dodatkowej
konstrukcji (bo z reguły im prościej tym lepiej) i tworzenie czegoś,
czego nie ma w innych językach (a więc zmiana jakichś przyzwyczajeń).
Obiektywnie patrząc wydaje mi się, że tak właśnie powinno być.

Mała dygresja:

Podobnie przydałoby się coś zrobić ogólnie z typem wskaźników. To znaczy
żeby można było zdefiniować funkcję, która działa na wskaźnikach
dowolnego typu przekazując typ tego wskaźnika dalej, ale sama ten w typ
wskaźnika nie patrzy i _dla każdego typu wskaźnika jej kod jest taki
sam_. Ewentualnie żeby była ważna sama wielkość obiektu wskazywanego
przez wskaźnik i kod działający na wskaźnikach różnych typów, ale tej
samej wielkości, był uwspólniony. Ewentualnie - inna możliwość - żeby
wielkość wskaźnika była przekazywana w niejawnym parametrze, a kod był
jeden dla wszystkich typów, który by korzystał z tej wielkości jak ze
zmiennej, a nie stałej.

W tej sprawie chodzi nie tylko o przekazywanie typu wskaźnika na
zewnątrz funkcji, ale również po prostu o używanie wskaźnika dowolnego
typu w jednej funkcji, bez związku z wynikiem funkcji. W sprawie consta
ta sprawa nie istnieje, bo jeśli nie przekazujemy wskaźnika na zewnątrz,
a tylko chcemy, żeby funkcja działała tak samo na wskaźnikach z constem
i bez consta, to można po prostu zdefiniować tylko wersję z constem -
wartość bez consta zawsze można potraktować jak wartość z constem,
konwersja typu w tę właśnie stronę jest zawsze automatyczna i
bezproblemowa. Żeby było tak samo prosto w przypadku wskaźników na różne
typy, potrzebny jest wskaźnik, na który można bezpiecznie skonwertować
każdy inny wskaźnik - jakiś taki void *. Problem tylko w tym, że ginie
informacja o wielkości oryginalnego wskaźnika i trzeba by ją przekazywać
ręcznie, jeśli działanie funkcji od tej wielkości zależy (mimo że nie
zależy od tego, jaki konkretnie to jest typ).

Nieco bardziej ogólnie: Szablony to fajna rzecz, ale mają taką wadę, że
rozdzielanie na poszczególne wersje zawsze jest już na etapie
kompilacji, nawet jeśli warto by zrobić mniej "instancji" szablonu,
które będą miały wielkości typów albo wskaźniki na użyte w środku
funkcje jako niejawne argumenty. Chciałbym jakoś zbliżyć do siebie jedno
z drugim. Żeby można było zadeklarować coś, co wygląda jak szablon, ale
zamiast rozbijania tego na każdą kombinację typów, kod był wspólny dla
różnych typów i ewentualne potrzebne różnice były przekazywane przez
niejawne argumenty. Ważne, żeby to wyglądało prawie tak samo jak
szablon, który swoją drogą powinien być w zapisie maksymalnie podobny do
zwykłej funkcji, której wszystkie typy argumentów są akurat znane.

Idealnie by było, gdyby kompilator zwykle sam rozpoznawał, że
skompilowany kod szablonu z argumentem typu "wskaźnik do dowolnego typu"
może być dzielony przez różne typy ("wskaźnik do dowolnego typu" oznacza
tutaj nie void *, tylko T *, gdzie T jest tym zmiennym typem szablonu;
dzięki temu można przekazywać ten typ bez zmian na zewnątrz funkcji).
Nie wiem, jak to jest w istniejących obecnie kompilatorach - czy
rozpoznają, że to można skompilować raz i używać tego samego kodu w
różnych sytuacjach. A dodatkowo można chcieć, żeby w przypadku, kiedy
wielkość tego typu ma już znaczenie, ale inne cechy typu już nie, taki
szablon mógł się skompilować w jednej wersji, która jako niejawny
argument miałaby przekazywaną wielkość tego typu. Osobne instancje
"rozwiązania" tego szablonu byłyby tworzone tylko jeśli dany typ na
konstruktor lub destruktor (ale to też można zrobić w jednej, jeśli
wskaźnik na ten konstruktor czy destruktor byłby przekazywany w
niejawnym argumencie).

Niektórzy mogą powiedzieć, że takie rzeczy są zwykle potrzebne tylko na
dość niskim poziomie (np. przy definiowaniu operatora przypisania,
indeksowania tablic albo przydzielania pamięci), więc spokojnie mogą być
wbudowane w kompilator. Ja jednak chcę, żeby jak najmniej tego typu
zachowań (bardzo szeroko rozumiejąc "tego typu zachowania" - dotyczy to
wielkiego mnóstwa innych spraw) było wbudowane w kompilator, bo to
działałoby tylko dla standardowych typów i nie można by implementować
własnych, nieco mniej typowych konstrukcji. Zaliczają się do tego typu
spraw (tzn. takich, które ja chcę umożliwić definiować własnoręcznie, a
nie wbudowywać raz na zawsze w kompilator) również konwersje typów (to
duży i dość trudny temat), funkcje obliczane na etapie kompilacji, jeśli
przypadkiem można, odpowiednik printfa (tutaj chodzi o wbudowywanie w
standardową bibliotekę, a nie w kompilator - ale też nie można łatwo
samemu tego rozszerzać), oczywiście definiowanie znaczeń operatorów,
przeciążanie i referencje (które to dwie ostatnie rzeczy w C są tylko w
przypadku odpowiednio standardowych operatorów i lewego argumentu
operatora przypisania - widać, że w C++ już pozwolili używać tego we
własnych konstrukcjach i te wbudowane operatory przestały być tak
uprzywilejowane), a także nawet zestaw i priorytety operatorów i ogólnie
składnia funkcji inna niż nazwa (arg1, arg2, arg3), tak jak w C/C++
specjalną składnię mają np. pętle, ale własnych takich funkcji nie można
definiować (o tym, tzn. o moim podejściu do budowy wyrażeń, będę jeszcze
opowiadał).

Kończąc dygresję:

(Można się próbować zastanawiać, czy przypadkiem nie wystarczą same
referencje, nawet bez przekazywania przez wartość. Ale jestem
zdecydowanie temu przeciwny, bo wtedy albo programy muszą się wykonywać
wolniej i nie da się pisać szybkiego kodu, albo proste typy wbudowane
mają specjalne prawa.)

Chciałem przy okazji zauważyć, że sam fakt, że dwie konstrukcje znaczą
to samo, o ile tylko obie mogą być użyte w danym miejscu, a różnią się
tylko tym, że czasem jest dozwolona tylko jedna, a czasem - tylko druga,
czasami oznacza, że należałoby te konstrukcje utożsamić, a czasami nie.
W tym przypadku bardzo dobrze, że wartości z constem i bez consta nie są
utożsamione, bo te zabronione możliwości w rzeczywistości są złe (nawet
w C/C++ jest zabronione trochę za mało, o czym mówiłem poprzednio;
zaznaczam, że nie mówię teraz o praktyce i konieczności zachowania
kompatybilności, tylko o idealnym rozwiązaniu w hipotetycznym nowym
języku). Tymczasem niedługo podzielę się na pl.comp.programming
spostrzeżeniami na zupełnie inny temat, w którym dwie konstrukcje też
znaczą zawsze to samo, o ile tylko mogą być akurat użyte wymiennie, i
różnią się tylko tym, że czasem jest dozwolona tylko jedna, a czasem
tylko druga - i tym razem uważam, że można by je utożsamić i w moim
języku będą wyglądały tak samo, bo przypadki, w których teraz można użyć
tylko jednej z nich, nie są w rzeczywistości niczym szczególnym i poza
jakimiś technicznymi sprawami nie ma istotnej różnicy między tymi
konstrukcjami, która usprawiedliwiałaby rozróżnienie. Konkretnie chodzi
o różnicę między {a; b; c;} a (a, b, c); w C/C++. Streszczenie
"prawidłowego" IMHO rozwiązania: {...} czy też begin...end zapisują się
nawiasami okrągłymi: (...), a operator "przecinek" z C jest tym samym
średnikiem, który zwykle oddziela instrukcje.

--
                                 QRCZAK      #
    __("<              Marcin Kowalczyk     #*#
    \__/              qrczak@knm.org.pl    #\#&#
     ^^      http://qrczak.home.ml.org/      I
Technical Details
Message-ID: <Pine.LNX.3.96.980110100249.26503F-100000@qrnik.knm.org.pl>
Path: mbox-mo1.pugleaf.net!news-archive.icm.edu.pl!pingwin.icm.edu.pl!news.icm.edu.pl!qrnik.knm.org.pl!qrnik.knm.org.pl!qrczak
References: <199712260255.DAA21573@sprocket.ict.pwr.wroc.pl> <Pine.LNX.3.96.971226092616.28295A-100000@qrnik.knm.org.pl> <34B4E719.69A1@imm.org.pl>

Show Full Headers
Complete Article Headers
Path: mbox-mo1.pugleaf.net!news-archive.icm.edu.pl!pingwin.icm.edu.pl!news.icm.edu.pl!qrnik.knm.org.pl!qrnik.knm.org.pl!qrczak
From: Qrczak <qrczak@knm.org.pl>
Newsgroups: pl.comp.lang.c,pl.comp.programming
Subject: Wskaźniki tylko do odczytu (Re: C.D. Segmentation
 fault przy strtok...)
Followup-To: pl.comp.programming
Date: Sat, 10 Jan 1998 12:05:02 +0100
Organization: Klub Nieszkodliwych Manjaków
Lines: 288
Message-ID: <Pine.LNX.3.96.980110100249.26503F-100000@qrnik.knm.org.pl>
References: <199712260255.DAA21573@sprocket.ict.pwr.wroc.pl> <Pine.LNX.3.96.971226092616.28295A-100000@qrnik.knm.org.pl> <34B4E719.69A1@imm.org.pl>
NNTP-Posting-Host: ppp3-cst30.warszawa.tpnet.pl
Mime-Version: 1.0
In-Reply-To: <34B4E719.69A1@imm.org.pl>
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit