Wie weit kann man mit einer stark typisierten Sprache gehen?

Lesezeit: 9 Minuten

Nehmen wir an, ich schreibe eine API, und eine meiner Funktionen nimmt einen Parameter, der einen Kanal darstellt, und wird immer nur zwischen den Werten 0 und 15 liegen. Ich könnte es so schreiben:

void Func(unsigned char channel)
{
    if(channel < 0 || channel > 15)
    { // throw some exception }
    // do something
}

Oder nutze ich den Vorteil, dass C++ eine stark typisierte Sprache ist, und mache mich selbst zu einem Typ:

class CChannel
{
public:
    CChannel(unsigned char value) : m_Value(value)
    {
        if(channel < 0 || channel > 15)
        { // throw some exception }
    }
    operator unsigned char() { return m_Value; }
private:
    unsigned char m_Value;
}

Meine Funktion wird jetzt zu dieser:

void Func(const CChannel &channel)
{
    // No input checking required
    // do something
}

Aber ist das total übertrieben? Ich mag die Selbstdokumentation und die Garantie, dass es das ist, was es sagt, aber lohnt es sich, den Bau und die Zerstörung eines solchen Objekts zu bezahlen, geschweige denn all das zusätzliche Tippen? Bitte teilen Sie mir Ihre Kommentare und Alternativen mit.

  • FWIW, da der Parametertyp ist unsignedes hat keinen Zweck zu testen channel < 0.

    – GManNickG

    5. Juli 2010 um 19:34 Uhr

  • Zufällige interessante Fakten: Ada unterstützt dies nativ, da type CChannel is range 0..15;und da war ein Papier vor ein paar Jahren über das Hinzufügen einer ähnlichen Fähigkeit zu C

    – Michael Mrozek

    5. Juli 2010 um 19:38 Uhr

  • @Michael: … und Ada hat es von Pascal bekommen, von dem es die Syntax nur geringfügig geändert hat (type CChannel = 0 .. 15;).

    – Jerry Sarg

    5. Juli 2010 um 20:03 Uhr

  • Nun, da Sie die Antwort auf Ihre gestellte Frage erhalten haben, haben Sie sich überlegt, ob Sie eigentlich eine haben sollten class Channel mit einem Mitglied Channel::Func()?

    – Phil Müller

    5. Juli 2010 um 21:07 Uhr

  • Warum machen Func ein Mitglied ? Das ist weder Java noch C#!

    – Matthias M.

    6. Juli 2010 um 6:30 Uhr

Benutzer-Avatar
GManNickG

Wenn Sie diesen einfacheren Ansatz wollten, verallgemeinern Sie ihn, damit Sie ihn besser nutzen können, anstatt ihn auf eine bestimmte Sache zuzuschneiden. Dann ist die Frage nicht “sollte ich eine ganz neue Klasse für diese spezielle Sache machen?” aber “sollte ich meine Dienstprogramme verwenden?”; Letzteres ist immer ja. Und Dienstprogramme sind immer hilfreich.

Also mach sowas wie:

template <typename T>
void check_range(const T& pX, const T& pMin, const T& pMax)
{
    if (pX < pMin || pX > pMax)
        throw std::out_of_range("check_range failed"); // or something else
}

Jetzt haben Sie bereits dieses nette Dienstprogramm zum Überprüfen von Bereichen. Ihr Code, auch ohne den Kanaltyp, kann bereits sauberer gemacht werden, indem Sie ihn verwenden. Sie können weiter gehen:

template <typename T, T Min, T Max>
class ranged_value
{
public:
    typedef T value_type;

    static const value_type minimum = Min;
    static const value_type maximum = Max;

    ranged_value(const value_type& pValue = value_type()) :
    mValue(pValue)
    {
        check_range(mValue, minimum, maximum);
    }

    const value_type& value(void) const
    {
        return mValue;
    }

    // arguably dangerous
    operator const value_type&(void) const
    {
        return mValue;
    }

private:
    value_type mValue;
};

Jetzt haben Sie ein nettes Dienstprogramm und können einfach Folgendes tun:

typedef ranged_value<unsigned char, 0, 15> channel;

void foo(const channel& pChannel);

Und es ist in anderen Szenarien wiederverwendbar. Stecken Sie einfach alles in ein "checked_ranges.hpp" Datei und verwenden Sie es, wann immer Sie es brauchen. Es ist nie schlecht, Abstraktionen zu machen, und Dienstprogramme in der Nähe zu haben, ist nicht schädlich.

Machen Sie sich auch keine Gedanken über die Gemeinkosten. Das Erstellen einer Klasse besteht einfach darin, denselben Code auszuführen, den Sie ohnehin ausführen würden. Außerdem ist sauberer Code allem anderen vorzuziehen; Leistung ist ein letztes Anliegen. Sobald Sie fertig sind, können Sie einen Profiler dazu bringen, zu messen (nicht zu raten), wo sich die langsamen Teile befinden.

  • +1. Wenn man bedenkt, wie ähnlich das dem ist, was ich gepostet habe, wäre es fast heuchlerisch, wenn ich nicht stimme zu! 🙂

    – Jerry Sarg

    5. Juli 2010 um 20:09 Uhr

  • +1, aber Sie könnten eine Art expliziten Typedef verwenden (ich weiß, dass Boost einen hat), sodass Sie Kompilierzeitfehler erhalten, wenn Sie ihn mit einem anderen ranged_value <0,15> verwechseln. Natürlich wird in diesem Fall die argumentierbare Überladung des Operators value_type entfernt.

    – Viktor Sehr

    5. Juli 2010 um 21:28 Uhr

  • Wenn Sie eine andere haben typedef ranged_value<unsigned char, 0, 15> potatoe;es besteht die Gefahr, dass Sie a passieren potatoe Instanz statt a channel Beispiel. Boost hat Strong Typedef dafür. Andernfalls gibt es immer die Lösung, einen 4. Diskriminanten-Template-Parameter zu verwenden, um ein Tag aus einem anonymen Namensraum zu übergeben.

    – Matthias M.

    6. Juli 2010 um 6:34 Uhr

  • @GMan: haha, ich liebe diesen Satz. In diesem Fall ist es aber recht einfach zu bewältigen 🙂

    – Matthias M.

    6. Juli 2010 um 13:09 Uhr

  • @MartinDrozdik: Macht es einfach einfacher, die Vorlagenargumente von außerhalb der Klasse zu erhalten.

    – GManNickG

    28. November 2012 um 17:02 Uhr

Benutzer-Avatar
Jerry Sarg

Ja, die Idee lohnt sich, aber (IMO) das Schreiben einer vollständigen, separaten Klasse für jeden Bereich von Ganzzahlen ist irgendwie sinnlos. Ich bin auf genug Situationen gestoßen, die Ganzzahlen mit begrenztem Bereich erfordern, dass ich zu diesem Zweck eine Vorlage geschrieben habe:

template <class T, T lower, T upper>
class bounded { 
    T val;
    void assure_range(T v) {
        if ( v < lower || upper <= v)
            throw std::range_error("Value out of range");
    }
public:
    bounded &operator=(T v) { 
        assure_range(v);
        val = v;
        return *this;
    }

    bounded(T const &v=T()) {
        assure_range(v);
        val = v;
    }

    operator T() { return val; }
};

Die Verwendung wäre so etwas wie:

bounded<unsigned, 0, 16> channel;

Natürlich können Sie noch ausführlicher werden, aber diese einfache Methode bewältigt immer noch ungefähr 90% der Situationen ziemlich gut.

  • +1, ich füge diese Frage zu meinen Favoriten für diese Antwort hinzu!

    – FrustriertMitFormularDesigner

    5. Juli 2010 um 19:55 Uhr

  • +1. Wenn man bedenkt, wie ähnlich das dem ist, was ich gepostet habe, wäre es fast heuchlerisch, wenn ich nicht stimme zu! 🙂 😛

    – GManNickG

    5. Juli 2010 um 20:21 Uhr

  • Jetzt, wo ich es mir noch einmal ansehe, wie kann es verwendet werden? Kann ich nicht machen channel c; c = 5; weil es keinen Standardkonstruktor hat, noch kann ich das tun channel c(5); weil es keinen Konvertierungskonstruktor hat.

    – GManNickG

    23. Oktober 2010 um 20:35 Uhr

  • Jetzt können Sie den Kopierzuweisungsoperator loswerden, da standardmäßig der Konstruktor verwendet wird, um das Objekt zu erstellen, von dem aus zugewiesen werden soll. (Dann passen unsere Kommentare wirklich zusammen. :P)

    – GManNickG

    23. Oktober 2010 um 20:51 Uhr

  • Sie können, aber ich bevorzuge es nicht – der Zuweisungsoperator ist eine ziemlich wichtige Optimierung (er vermeidet die Bereichsprüfung, wenn ein Wert zugewiesen wird, von dem wir wissen, dass er innerhalb des Bereichs liegt).

    – Jerry Sarg

    23. Oktober 2010 um 21:00 Uhr

Nein, es ist kein Overkill – Sie sollten immer versuchen, Abstraktionen als Klassen darzustellen. Es gibt eine Million Gründe dafür, und der Overhead ist minimal. Ich würde die Klasse jedoch Channel nennen, nicht CChannel.

Ich kann nicht glauben, dass bisher niemand Enums erwähnt hat. Gibt Ihnen keinen kugelsicheren Schutz, ist aber immer noch besser als ein einfacher Integer-Datentyp.

Sieht nach Overkill aus, besonders die operator unsigned char() Accessor. Sie kapseln keine Daten, Sie machen offensichtliche Dinge komplizierter und wahrscheinlich fehleranfälliger.

Datentypen wie Ihre Channel sind normalerweise ein Teil von etwas Abstrakterem.

Also, wenn Sie diesen Typ in Ihrem verwenden ChannelSwitcher Klasse, könnten Sie kommentierte typedef direkt in der verwenden ChannelSwitcher‘s Körper (und wahrscheinlich wird Ihre typedef so sein public).

// Currently used channel type
typedef unsigned char Channel;

  • Ich denke der typedef ist ein guter Weg, damit umzugehen, es sei denn a Channel wird sein eigenes kanalspezifisches Verhalten haben (wie z open, connect, send…), oder ob es eine wahrscheinliche zukünftige Möglichkeit gibt, Unterklassen zu benötigen, wenn Channel.

    – FrustriertMitFormularDesigner

    5. Juli 2010 um 19:39 Uhr


  • @Frustriert Ich denke eigentlich, dass der zweite Fall die Klassennutzung ziemlich offensichtlich macht 🙂

    – M.Williams

    5. Juli 2010 um 19:41 Uhr

  • @FrustratedWithFormsDesigner – Die Channel-Klasse wird wahrscheinlich nur diesen Wert enthalten. @Kotti – Ich muss die Typedef immer noch einschränken, aber ja, das scheint meiner ersten Lösung vorzuziehen.

    – Dan Dan

    5. Juli 2010 um 19:44 Uhr

Benutzer-Avatar
Tom Crockett

Ob Sie beim Erstellen Ihres “CChannel”-Objekts oder am Eingang der Methode, die die Einschränkung erfordert, eine Ausnahme auslösen, macht kaum einen Unterschied. In beiden Fällen machen Sie Laufzeit-Assertionen, was bedeutet, dass Ihnen das Typsystem wirklich nichts nützt, oder?

Wenn Sie wissen wollen, wie weit Sie kann Wenn Sie mit einer stark typisierten Sprache arbeiten, lautet die Antwort “sehr weit, aber nicht mit C++”. Die Art von Leistung, die Sie benötigen, um eine Einschränkung wie „Diese Methode darf nur mit einer Zahl zwischen 0 und 15 aufgerufen werden“ statisch durchzusetzen, erfordert etwas namens abhängige Typen–das ist, Typen, die von Werten abhängen.

Um das Konzept in eine Pseudo-C++-Syntax zu packen (vorgeben, dass C++ abhängige Typen hätte), könnten Sie Folgendes schreiben:

void Func(unsigned char channel, IsBetween<0, channel, 15> proof) {
    ...
}

Beachten Sie, dass IsBetween wird parametriert durch Werte eher, als Typen. Um diese Funktion nun in Ihrem Programm aufzurufen, müssen Sie dem Compiler das zweite Argument übergeben, proofdie den Typ haben muss IsBetween<0, channel, 15>. Das heißt, man muss beweisen zur Kompilierzeit das channel liegt zwischen 0 und 15! Diese Vorstellung von Typen, die Sätze darstellen, deren Werte Beweise dieser Sätze sind, heißt die Curry-Howard-Korrespondenz.

Natürlich kann es schwierig sein, solche Dinge zu beweisen. Abhängig von Ihrer Problemdomäne kann das Kosten-Nutzen-Verhältnis leicht zugunsten einer bloßen Ausführung von Laufzeitprüfungen für Ihren Code kippen.

  • Ich denke der typedef ist ein guter Weg, damit umzugehen, es sei denn a Channel wird sein eigenes kanalspezifisches Verhalten haben (wie z open, connect, send…), oder ob es eine wahrscheinliche zukünftige Möglichkeit gibt, Unterklassen zu benötigen, wenn Channel.

    – FrustriertMitFormularDesigner

    5. Juli 2010 um 19:39 Uhr


  • @Frustriert Ich denke eigentlich, dass der zweite Fall die Klassennutzung ziemlich offensichtlich macht 🙂

    – M.Williams

    5. Juli 2010 um 19:41 Uhr

  • @FrustratedWithFormsDesigner – Die Channel-Klasse wird wahrscheinlich nur diesen Wert enthalten. @Kotti – Ich muss die Typedef immer noch einschränken, aber ja, das scheint meiner ersten Lösung vorzuziehen.

    – Dan Dan

    5. Juli 2010 um 19:44 Uhr

Benutzer-Avatar
SCFranzösisch

Ob etwas übertrieben ist oder nicht, hängt oft von vielen verschiedenen Faktoren ab. Was in einer Situation übertrieben sein mag, ist es in einer anderen nicht.

Dieser Fall ist vielleicht nicht übertrieben, wenn Sie viele verschiedene Funktionen hatten, die alle Kanäle akzeptierten und alle die gleiche Reichweitenprüfung durchführen mussten. Die Channel-Klasse würde Codeduplizierung vermeiden und auch die Lesbarkeit der Funktionen verbessern (ebenso wie die Benennung der Klasse Channel statt CChannel – Neil B. hat Recht).

Manchmal, wenn der Bereich klein genug ist, definiere ich stattdessen eine Aufzählung für die Eingabe.

1012300cookie-checkWie weit kann man mit einer stark typisierten Sprache gehen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy