Inkrementace ne levé i pravé straně přiřazení

Inkrementace ne levé i pravé straně přiřazení
« kdy: 05. 09. 2019, 11:57:52 »
Zdravím.

Mohl by mi někdo vysvětlit vyhodnocení tohoto příkazu? Prosím o vysvětlení krok za krokem. Co se vyhodnotí nejprve a co potom a co nakonec. Vím, že je to šílenost, která se nedoporučuje, ale přeci jen jsem na tento problém narazil.

Příkaz je:
Kód: [Vybrat]
i = 0;
pole[i++] = ++i; /* JAK SE VYHODNOTI TENTO PRIKAZ? */

a zde ještě přikládám celý zdroják (pro vyzkoušení):

Kód: [Vybrat]
#include <stdio.h>
#include "test.h"

#define MAX 5

int main()
{
int pole[MAX];
int i, k;

/* vynuluj pole[] */
for(k = 0; k < MAX; ++k)
pole[k] = 0;

i = 0;
pole[i++] = ++i; /* JAK SE VYHODNOTI TENTO PRIKAZ? */

/* vypis vysledek */
printf("i = %d\n", i);
for(k = 0; k < MAX; ++k)
printf("pole[%d] = %d\n", k, pole[k]);
}

a výsledek výpisu je:

i = 2
pole[0] = 0
pole[1] = 2
pole[2] = 0
pole[3] = 0
pole[4] = 0



Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #1 kdy: 05. 09. 2019, 12:19:39 »
Zapomněl jsem napsat, že jde o jazyk C.

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #2 kdy: 05. 09. 2019, 12:57:33 »
Nedělej to. Prostě ne.

To chování není definované, protože přiřazení není "sequence point".

alex6bbc

  • *****
  • 1 431
    • Zobrazit profil
    • E-mail
Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #3 kdy: 05. 09. 2019, 13:17:16 »
ma pravdu nedelej to!!!

vygeneroval jsem si assembler, ktery to vygenerovalo v gcc -S.
zkusil jsem ruzne varianty x[i++] = i++ s prefixovym/postfixovym a assemblery se lisi.

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #4 kdy: 05. 09. 2019, 13:30:59 »
Tak uz jsem odpoved nasel. Vyhodnoceni vyse zminovaneho prikazu zalezi na kompilatoru a architekture pocitace, na kterem program bezi. Uz jen takovyto priklad:

Kód: [Vybrat]
pole[i] = ++i;
ma stejny problem. V knizce se o tom hovori jako o "vedlejsim efektu". A radi se tam, aby se tomu programatori vyhibali. Pokud programator vi, jak se to zkompiluje, "muze" to pouzit. Ale zasadne se to nedoporucuje.

Takze odpoved zni, jak jste to uz rekli, NEDELAT TO!

Dekuji za odpovedi.


Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #5 kdy: 05. 09. 2019, 14:02:00 »
zkusil jsem ruzne varianty x[i++] = i++ s prefixovym/postfixovym a assemblery se lisi.

Tak to je snad pochopitelné, když se pre/post liší samy o sobě.

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #6 kdy: 05. 09. 2019, 16:07:12 »
Vyhodnoceni vyse zminovaneho prikazu zalezi na kompilatoru a architekture pocitace, na kterem program bezi.
Bacha. Je to ještě daleko horší než že se ty inkrementy vyhodnotí v nějakém neznámém pořadí.

Je to "nedefinované chování". Na tenhle pojem si dej bacha. Překladač může optimalizovat kód tak, jako by v něm žádné nedefinované chování nemohlo nastat. To znamená, že se tam pak může dít něco, co ten kód ani vzdáleně nepřipomíná. Překladač může klidně vyházet celé bloky kódu. Taky se mi třeba stalo že if, co vybíral ze dvou hodnot, mi díky nedefinovanému chování vrátil úplně jinou třetí hodnotu.

Citace
Pokud programator vi, jak se to zkompiluje, "muze" to pouzit.
Problém je v tom, že programátor neví jak se to zkompiluje. A neví to proto, že si takové chování není schopen představit ani ve svých nejdivočejších snech. ;)

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #7 kdy: 05. 09. 2019, 20:27:47 »
Jen doplním, že od C++17 je vyhodnocení takového výrazu definované, nicméně souhlasím s ostatními, že je lepší to nedělat. Viz Stricter order of expression evaluation: http://www.cplusplus2017.info/c17-global-changes/

BoneFlute

  • *****
  • 1 981
    • Zobrazit profil
Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #8 kdy: 06. 09. 2019, 03:55:26 »
Příkaz je:
Kód: [Vybrat]
i = 0;
pole[i++] = ++i; /* JAK SE VYHODNOTI TENTO PRIKAZ? */

Hustý. Mě to přijde jasný. Do pole na indexu 1 ulož 1. A ve výsledku je i = 2. Takhle bych ten výraz chápal, a cokoliv jiné mi přijde divné.

Což ale samozřejmě vůbec nic neznamená, a C se nemusí řídit tím jak to chápu já :-)

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #9 kdy: 06. 09. 2019, 06:59:40 »
Příkaz je:
Kód: [Vybrat]
i = 0;
pole[i++] = ++i; /* JAK SE VYHODNOTI TENTO PRIKAZ? */

Hustý. Mě to přijde jasný. Do pole na indexu 1 ulož 1. A ve výsledku je i = 2. Takhle bych ten výraz chápal, a cokoliv jiné mi přijde divné.

Což ale samozřejmě vůbec nic neznamená, a C se nemusí řídit tím jak to chápu já :-)

Tak jako já vidím minimálně dvě možnosti.

Do indexu 0 uložím 2 a mám na konci 2.
Nebo do indexu 1 uložím 1 a mám na konci 2.

Na čem se můžeme asi shodnout, tak je že po tom řádku bude hodnota proměnné i = 2. :)

Stačí aby se jednoznačnně definovalo jaká strana se bude vyhodnocovat dříve (jestli chci dříve znát adresu nebo výsledek pro přiřazení). Nu ale tak podle všeho to definované asi není, když se tu o tom debatuje. :) Respektive jak již bylo napsáno, tak C++17 to definuje (nejdříve hodnota a pak adresa), ale vzhledem k tomu, že to je asi standard pro C++, tak nevím jak moc je to respektováno v C.

Ale nejjednoduší jak nemít problém - nepoužívat to.

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #10 kdy: 06. 09. 2019, 08:35:35 »
a výsledek výpisu je:

i = 2
pole[0] = 0
pole[1] = 2
pole[2] = 0
pole[3] = 0
pole[4] = 0

To je fakt zajímavý, čím jsi to překládal a s jakými volbami?

Jak říká Milan, logické by mi přišlo
Do indexu 0 uložím 2 a mám na konci 2.
Nebo do indexu 1 uložím 1 a mám na konci 2.
ale tohle teda ne, to je vyloženě "náhoda" :)

Pro zajímavost:

Kód: [Vybrat]
$ cc main.c
main.c:15:8: warning: multiple unsequenced modifications to 'i' [-Wunsequenced]
        pole[i++] = ++i;        /* JAK SE VYHODNOTI TENTO PRIKAZ? */
              ^     ~~
1 warning generated.

$ ./a.out
i = 2
pole[0] = 0
pole[1] = 1
pole[2] = 0
pole[3] = 0
pole[4] = 0

$ cc -v
Apple LLVM version 10.0.1 (clang-1001.0.46.4)
Target: x86_64-apple-darwin18.7.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

Kód: [Vybrat]
$ gcc main.c

$ ./a.out
i = 2
pole[0] = 2
pole[1] = 0
pole[2] = 0
pole[3] = 0
pole[4] = 0

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/arm-linux-gnueabihf/6/lto-wrapper
Target: arm-linux-gnueabihf
Configured with: ../src/configure -v --with-pkgversion='Debian 6.3.0-18+deb9u1' --with-bugurl=file:///usr/share/doc/gcc-6/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-6 --program-prefix=arm-linux-gnueabihf- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-libitm --disable-libquadmath --enable-plugin --enable-default-pie --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-6-armhf/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-6-armhf --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-6-armhf --with-arch-directory=arm --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-sjlj-exceptions --with-arch=armv7-a --with-fpu=vfpv3-d16 --with-float=hard --with-mode=thumb --enable-checking=release --build=arm-linux-gnueabihf --host=arm-linux-gnueabihf --target=arm-linux-gnueabihf
Thread model: posix
gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)

Kód: [Vybrat]
$ gcc main.c

$ ./a.out
i = 2
pole[0] = 0
pole[1] = 2
pole[2] = 0
pole[3] = 0
pole[4] = 0

$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.4.0-1ubuntu1~18.04.1' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #11 kdy: 06. 09. 2019, 09:12:36 »
Příkaz je:
Kód: [Vybrat]
i = 0;
pole[i++] = ++i; /* JAK SE VYHODNOTI TENTO PRIKAZ? */

Hustý. Mě to přijde jasný. Do pole na indexu 1 ulož 1. A ve výsledku je i = 2. Takhle bych ten výraz chápal, a cokoliv jiné mi přijde divné.

Což ale samozřejmě vůbec nic neznamená, a C se nemusí řídit tím jak to chápu já :-)
C je jazyk z kompilátorové doby kamenné. V době, kdy vznikalo, se překládalo velice přímočaře do instrukcí z našeho pohledu velice obskurních architektur. :) Třeba takové pdp-11 AFAIK umělo postinkrement jako součást adresy v rámci instrukce.
Ta podezřelá konstrukce musí udělat dvě věci : spočítat adresu a spočítat hodnotu. Pro oboje může mít daná architektura nějaké specializované instrukce. Takže v té době dávalo smysl ten jazyk navrhnout tak, aby ty instrukce šlo použít. Specifikované je pořadí vyhodnocení hodnot, ale vedlejší efekty se prostě provedou někdy během toho. Jisté je akorát to, kdy už budou určitě hotové.

Když tohle přeložíte v clangu, tak bude dokonce vyhazovat warningy.

V C jsou i divnější věci. Inty třeba vůbec nemusí být reprezentované dvojkovým doplňkem. Z dnešního pohledu je to už kapku úlet. Takže mimálně nové verze C++ se to snaží trochu učesat. (Např. http://www.open-std.org/jtc1/sc22/wg14/www/docs/n2218.htm)

Třeba přetečení integerů je a bude stále nedefinovaná operace. Kdyby překladač musel počítat s přetečením integerů, pak nemůže dělat spoustu (z našeho pohledu samozřejmých) aritmetických úprav výrazů. Ale past na začátečníky to teda je.

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #12 kdy: 06. 09. 2019, 09:15:47 »
Specifikované je pořadí vyhodnocení hodnot
Ani pořadí evaluace argumentů funkce není definované, ne?

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #13 kdy: 06. 09. 2019, 09:27:38 »
Specifikované je pořadí vyhodnocení hodnot
Ani pořadí evaluace argumentů funkce není definované, ne?
Není. Tady je IMO důvod celkem jasný. Různé volací konvence ukládají ty parametry na zásobník v různém pořadí. A C chce mít možnost dělat výpočet-push-výpočet-push... bez následného přerovnávání.

To pořadí vyhodnocení je nejen nespecifikované, ale výpočty jednotlivých parametrů dokonce můžou být proložené. Až C++17 to trochu omezilo. Parametry se sice stále vyhodnocují v neznámém pořadí, ale už bez prokládání. Překladač ty výpočty může stále přeskládat, ale už to nesmí být vidět navenek.

Re:Inkrementace ne levé i pravé straně přiřazení
« Odpověď #14 kdy: 06. 09. 2019, 10:41:28 »
Nic "nedefinovaneho" na tom neni. ++ je normalni funkce, akorat zapsana jako operator. Takze:

++i si prepiste jako
Kód: [Vybrat]
int incr_pre(int &i) { i++; return i; }
i++ prepiste jako
Kód: [Vybrat]
int incr_pos(int &i) { int tmp = i; i++; return tmp; }
a je to jasny.

Kód: [Vybrat]
a[i] = incr_pos(i); // a[i] = i++;
a[i] = incr_pre(i); // a[i] = ++i;
a[incr_pos(i)] = incr_pre[i]; // a[i++] = ++i;

Pro hnidopichy, ano neni to C, je to C++. Kdo chce C, at si prepise referenci na pointery.