Wie vergleicht ein Programm Boolean-Values?



  • Hey Leute, ich habe vor kurzem versucht ein kleines Programm zu schreiben und bin dann auf einer anderen Seite auf diese Lösung gestoßen. Ich verstehe zwar die Logik, aber nicht wie genau in der if-Abfrage festgestellt wird, dass "isPrime = true bzw. false" ist.

    #include <iostream>
    using namespace std;
    
    int main()
    {
      int n, i;
      bool isPrime = true;
    
      cout << "Enter a positive integer: ";
      cin >> n;
    
      for(i = 2; i <= n / 2; ++i)
      {
          if(n % i == 0)
          {
              isPrime = false;
              break;
          }
      }
      if (isPrime) //hier wird abgefragt, ob isPrime = true or false ist
          cout << "This is a prime number";
      else //das hier wird ausgegeben, wenn isPrime != true ist, aber wie stellt das Programm das fest? Nimmt es den Anfangswert von isPrime und vergleicht ihn mit dem neuen Wert?
          cout << "This is not a prime number";
    
      return 0;
    }
    


  • @twoplustwoisfour sagte in Wie vergleicht ein Programm Boolean-Values?:

      if (isPrime) //hier wird abgefragt, ob isPrime = true or false ist
    

    isPrime ist der Ausdruck der ausgewertet wird. Der Ausdruck gibt entweder true oder false. Man könnte natürlich auch isPrime == true schreiben, aber das wäre einfach nur doppelt gemoppelt.



  • @twoplustwoisfour
    Kleiner Zusatz zu dem was @Swordfish schon geschrieben hat: in C und C++ können alle Werte von "einfachen, eingebauten" Typen (Integers, Floats, Pointer) implizit nach bool convertiert werden. Die Regel dabei ist immer wenn etwas 0 bzw. 0.0 bzw. NULL ist, dann ist es false, und alles andere ist true.

    Daher funktioniern auch Sachen wie

    int* p = static_cast<int*>(malloc(sizeof int));
    if (p) {
        // p != nullptr
    } else {
        // p == nullptr
    }
    

    Weiters bieten viele Klassen eine explizite Konvertierung nach bool an, z.B. die Smart-Pointer Klassen (std::unique_ptr etc.). Da die Konvertierung nach bool innerhalb eines if als "explizit" gilt, können auch diese verwendet werden. D.h. du kannst auch schreiben

    std::unique_ptr<Foo> p = getFoo();
    if (p) {
        ...
    


  • @twoplustwoisfour Ein Ausdruck ist alles, was einen Wert liefert.

    Eine Variable oder eine Berechnung oder das Ergebnis einer Funktion.
    Also alles was rechts vom = bei einer Zuweisung steht.

    Ein Vergleich isprime == true ist auch nur ein Ausdruck.



  • @Swordfish Wenn ich nach der ersten if-Abfrage den Wert isPrime = false erhalte und dann die for-Schleife mit break beendet wird, dann wird ja zusätzlich abgefragt: "if(isPrime)... else...". Hier frage ich mich, ob isPrime jetzt false ist oder nicht, denn wenn die erste Abfrage richtig war, dann müsste der neue Wert von isPrime ja jetzt false sein, geschrieben wird aber nur "if(isPrime)", was mich denken lässt: if(isPrime == false), obwohl das ja keinen Sinn ergeben würde. Es sieht deshalb so aus, als ob hier der neue Wert mit dem alten verglichen wird. Sehe ich das richtig?



  • @twoplustwoisfour sagte in Wie vergleicht ein Programm Boolean-Values?:

    Sehe ich das richtig?

    Nein! if(isPrime) ist logisch dasselbe wie if(isPrime == true). das wurde auch schon mehrfach so gesagt.



  • @manni66 Hab es gerade herausgefunden! Peinlich 😃

    Danke



  • Hast du auch mal überlegt, warum du schreiben kannst:
    if (a == b) { ... } anstelle von if ((a == b) == true) { ... }?
    Du kannst sogar schreiben:if (((a == b) == true) == true) { ... }
    Das kannst du weitertreiben, es wird aber nur komplizierter.

    isPrime ist doch bereits ein bool! Das heißt, das Ding ist wahr oder falsch. wahr == wahr ist wahr. Das macht es auch nur komplexer!



  • @wob Perfekte Erklärung! Den Gedankengang hatte ich vorher nicht übertragen, aber jetzt ist es wirklich einfach.



  • unrelated

    @hustbaer sagte in Wie vergleicht ein Programm Boolean-Values?:

    int* p = static_cast<int*>(malloc(sizeof int));
    

    Das ist doch dasselbe Problem daß wir gerade andernorts haben. Ein p der auf etwas zeigt das es nicht gibt.
    Ich habe versucht dazu p0593r6 zu lesen aber davon Kopfschmerzen bekommen.



  • @Swordfish Für eingebaute Typen muss es da mMn. eine Ausnahme geben. Ich weiss nicht ob es sie gibt, aber ich weiss dass wenn es sie nicht gibt ein verdammt riesiger Haufen an Code UB hätte. Und das will man denke ich nicht.



  • Ja, will man nicht. Deswegen gibt es wahrscheinlich p0593r6.



  • Ich hatte das eigentlich schon so interepretiert, dass es vom Wording her tatsächlich UB wäre, aber es keinen interessiert, weil das Wording Quatsch war.
    Muss man wahrscheinlich abwarten, ob @Columbo über diesen Thread stolpert, er weiß sowas ^^



  • @Mechanics Ganz ehrlich? Wenn er den Durchblick(tm) hätte, hätte er sich schon längst gemeldet ^^

    // edit: Was jetzt keine Kritik sein soll ... das Wording muss auf jeden Fall gefixt werden, da blickt doch kein normalsterblicher mehr durch.


  • Mod

    Ich weiss nicht ob es sie gibt, aber ich weiss dass wenn es sie nicht gibt ein verdammt riesiger Haufen an Code UB hätte. Und das will man denke ich nicht.

    Darueber habe ich vor Jahren mit dem Autor auf freenode unterhalten. Er hat natuerlich bekraeftigt das viel existierender Code diese strikten Regeln verletzt. Und das die urspruengliche Idee hinter P0137 war, formelle Regeln aufzustellen, und die Luecken danach zu flicken.†

    P0593 loest das Problem mit einem retrograden Ansatz; statt viele Objekte aller moeglichen Typen provisorisch zu kreieren (was wahrscheinlich zu sehr vielen formalen Komplikationen fuehren wuerde), wird nur eines genommen, welches dem Programm definiertes Verhalten zusprechen wuerde. Das funktioniert, weil ein Objekt letztlich nur eine Menge von Bytes mit latenten Eigenschaften ist, und die eigentliche Semantik des Programs durch das Typsystem gewaehrleistet wird. Wenn ein Objekt vom Typ X wohldefiniert als int angesprochen werden darf, und die Semantik der int-Operationen vollstaendig befolgt wird, spielt der wahre Typ von X keine Rolle mehr. (Von der Tatsache mal abgesehen, dass der Typ wegen strict aliasing eben letztlich doch eingeschraenkt ist, aber ganz eindeutig ist es trotzdem nicht).

    Und die Regeln wurden doch eindeutig formuliert: malloc erzeugt Objekte, und folglich wird ein Objekt vom Typ int erzeugt und ein (kompatibler) Zeiger darauf vom Ausdruck ergeben. Und static_cast behaelt die Assoziation des Zeigers zum Objekt auch bei.

    † Um es in Yakk's Worte zu fassen: "The horse is out of the barn. Has been for 20 years." (Oder irgendwie sowas. Ich glaub unsere Diskussion wurde geloescht. Und 20 Jahre ist eine krasse Unterschaetzung, denn wir uebernehmen hier letztlich den modus operandi von C, welches in der Form schon seit ~50 Jahren verwendet wird



  • @Columbo
    Ja, bloss ist P0593 noch nicht abgesegnet 😉

    BTW: Weisst du wie man korrekt tut um etwas was man per new char[] bekommen und dann zweckentfremdet hat wieder zu löschen? Also

    char* mem = new char[2 * sizeof T]; // Aktiviert die magische auto-Objektifizierung
    // hier entsteht automagisch das T[] das nötig ist um dem Programm definiertes Verhalten zu geben,
    // allerdings ohne die Ts zu konstruieren
    T* arr = reinterpret_cast<T*>(mem); // OK
    new (&arr[0]) T(args...); // OK
    new (&arr[1]) T(args...); // OK
    arr[0].useT(); // OK
    arr[1].useT(); // OK
    arr[1].~T(); // OK
    arr[0].~T(); // OK
    // Und wie werden wir den Speicher jetzt wieder los?
    

    Meine Vermuting ist

    new(arr) std::byte[2 * sizeof T]; // Ändert keine Bytes weil ja kein Initializer, reaktiviert aber die magische auto-Objektifizierung
    // hier entsteht automagisch das char[] das nötig ist um dem Programm definiertes Verhalten zu geben
    delete [] reinterpret_cast<char*>(arr); // OK
    

    aber so richtig klar geht das aus dem Proposal mMn. nicht hervor.


  • Mod

    @hustbaer

    T* arr = reinterpret_cast<T*>(mem); // OK
    new (&arr[0]) T(args...); // OK
    new (&arr[1]) T(args...); // OK
    arr[0].useT(); // OK
    arr[1].useT(); // OK
    

    Ist technisch inkorrekt, weil arr niemals auf ein T Objekt zeigt. Aber vor allem deshalb, weil auch der Zeiger auf das erste T Objekt nicht auf ein Array zeigt; arr + 1 ist ein pointer past-the-end, und arr[1] damit undefiniert. Allerdings ist das selbe Verhalten in vector zu finden, weshalb diese Regeln letztlich ueberarbeitet werden muessen.

    Zur eigentlichen Frage bzgl pointer provenance, [new.delete.array]p11:

    Preconditions: ptr is a null pointer or its value represents the address of a block of memory allocated by an earlier call to a (possibly replaced) operator new[](std​::​size_­t) [...]

    [basic.compound]p3:

    A value of a pointer type that is a pointer to or past the end of an object represents the address of the first byte in memory ([intro.memory]) occupied by the object [...]

    Ich interpretiere das wie folgt: Das erste Objekt vom Typ T liegt am ersten Byte des durch den frueheren operator new[] Aufruf allozierten Speichers. Der Zeiger arr repraesentiert also die Adresse dieses Speichers. Deshalb ist

    ::operator delete[](arr);
    

    IMO wohldefiniert.

    Fuer den delete[]-Ausdruck sind die Regeln scheinbar differenzierter.

    In an array delete expression, the value of the operand of delete may be a null pointer value or a pointer value that resulted from a previous array new-expression. If not, the behavior is undefined.

    Das legt streng genommen aus, dass der Zeiger den gleichen Zustand haben muss wie der, welcher vom zugehörigen operator new[] Aufruf ergeben worden ist. Dieser Code:

    delete [] reinterpret_cast<char*>(arr); 
    // Warum nicht gleich"delete [] mem"?
    

    Ist aber korrekt, weil arr mangels launder o.ae. immer noch auf das selbe Array wie mem zeigt, egal wie oft du den Zeiger castest †. Das hat mit der Zeile darueber aber nichts zu tun, denn sie veraendert den Wert von arr nicht, und sie erschafft ueberfluessigerweise ein char-Array, weil das auf welches mem zeigt immer noch existiert: ebenjenes Array stellt den Speicher fuer die T Objekte bereit (provides storage for).

    Und letztlich waere auch das hier ok (ich wuerde meine Hand dafuer nicht hinhalten...):

    T* arr = new (mem) T(args...);
    new (arr + 1) T(args...);
    delete [] arr;
    

    Der Zeigerwert ist vom placement new[] Aufruf, die Adresse ist vom vorherigen operator new[]() Aufruf. Clang erhebt hier eine Warnung, da arr ja nicht mittels new[] kreiert worden ist, aber der Standard schreibt explizit vor, dass nicht die Syntax, sondern der tatsaechliche Typ des Pointees eine Rolle spielt.

    static_cast aendert den Wert eines Zeigers per se nicht [1][2]:

    A prvalue of type “pointer to cv T”, where T is an object type, can be converted to a prvalue of type “pointer to cv void”. The pointer value ([basic.compound]) is unchanged by this conversion.

    A prvalue of type “pointer to cv1 void” can be converted to a prvalue of type “pointer to cv2 T” [..]. If the original pointer value represents the address A of a byte in memory and A does not satisfy the alignment requirement of T, then the resulting pointer value is unspecified. Otherwise, if the original pointer value points to an object a, and there is an object b of type T (ignoring cv-qualification) that is pointer-interconvertible with a, the result is a pointer to b. Otherwise, the pointer value is unchanged by the conversion.


  • Mod

    @hustbaer sagte in Wie vergleicht ein Programm Boolean-Values?:

    Ja, bloss ist P0593 noch nicht abgesegnet

    Soweit ich sehen kann, ist das wording doch schon im draft?



  • @Columbo sagte in Wie vergleicht ein Programm Boolean-Values?:

    @hustbaer

    T* arr = reinterpret_cast<T*>(mem); // OK
    new (&arr[0]) T(args...); // OK
    new (&arr[1]) T(args...); // OK
    arr[0].useT(); // OK
    arr[1].useT(); // OK
    

    Ist technisch inkorrekt, weil arr niemals auf ein T Objekt zeigt. Aber vor allem deshalb, weil auch der Zeiger auf das erste T Objekt nicht auf ein Array zeigt; arr + 1 ist ein pointer past-the-end, und arr[1] damit undefiniert. Allerdings ist das selbe Verhalten in vector zu finden, weshalb diese Regeln letztlich ueberarbeitet werden muessen.

    Das hab ich in http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/p0593r3.html anders verstanden. Da steht unter "This suggests that the largest set of types we could apply this to is" auch "Array types (with any element type)". Und unmittelbar davor der Absatz

    Note that we’re only interested in properties of the object itself here, not of its subobjects. In particular, the above two properties always hold for array types. While creating or destroying array elements might run code, creating the array object (without its elements) does not.

    Ich hab das so verstanden dass da wenn nötig auch ein Array automagisch entsteht, nur halt eines wo die eigentlichen Elemente noch nicht konstruiert worden sind.

    Dieser Code:

    delete [] reinterpret_cast<char*>(arr); 
    // Warum nicht gleich"delete [] mem"?
    

    Ist aber korrekt, weil arr mangels launder o.ae. immer noch auf das selbe Array wie mem zeigt, egal wie oft du den Zeiger castest †. Das hat mit der Zeile darueber aber nichts zu tun, denn sie veraendert den Wert von arr nicht, und sie erschafft ueberfluessigerweise ein char-Array, weil das auf welches mem zeigt immer noch existiert: ebenjenes Array stellt den Speicher fuer die T Objekte bereit (provides storage for).

    Diese Auslegung (also dass das "provides storage for" char array gleichzeitig mit dem T[] existiert) finde ich gewagt. Wenn das Kommittee das so verstehen will, OK. Aber checken tut das keiner mehr.

    Ah, ja: delete [] mem deswegen nicht, weil man mem üblicherweise nicht mehr hat wenn man nen vector o.ä. implementiert. Es sei denn man hebt sich nur den char*/std::byte* auf.

    Und letztlich waere auch das hier ok (ich wuerde meine Hand dafuer nicht hinhalten...):

    T* arr = new (mem) T(args...);
    new (arr + 1) T(args...);
    delete [] arr;
    

    Der Zeigerwert ist vom placement new[] Aufruf, die Adresse ist vom vorherigen operator new[]() Aufruf. Clang erhebt hier eine Warnung, da arr ja nicht mittels new[] kreiert worden ist, aber der Standard schreibt explizit vor, dass nicht die Syntax, sondern der tatsaechliche Typ des Pointees eine Rolle spielt.

    Halte ich für noch viel gewagter. Jetzt mal implementierungstechnisch betrachtet: Ist denn garantiert dass das mit dem Ermitteln der Anzahl der Objekte die zerstört werden müssen in dem Fall auch funktioniert? Klar, die vernünftigste Variante wird vermutlich sein nur die Grösse in Bytes für jeden Block abzuspeichern und sich dann die Anzahl der zu zerstörenden Objekte auszurechnen. (Andrerseits erfordert das ne Division, was auch nicht toll ist.) Aber garantiert der Standard das auch irgendwie (wenn dann vermutlich über 1000 Umwege)?


Log in to reply