Kategorien
C++Programmierung

TOP 5 erstaunlichster C++20 Syntax

Willkommen 👋 zu meinem 🎄🎅🎁 Weihnacht 🎄🎅🎁✨ –Spezial✨ ❆☃!

Heute präsentiere ich meine persönlichen TOP 5 der erstaunlichsten C++20 Syntax-Konstrukte.

Ich hoffe, es gefällt dir und du siehst vielleicht etwas Neues und/oder Interessantes.

Los geht’s:

Nummer 5

[](){}();

Diese Zeile erzeugt ein leeres Lambda und führt es direkt aus. Effektiv betrachtet, ist es eine No-Op. Kann ein leeres Lambda für etwas nützliches eingesetzt werden?

Ja, kann es. Ich werde ein nützliches Beispiel zeigen. Ein leeres Lambda könnte als no-op default für einer std::function dienen. Da jedes Lambda einen eigenen Typ hat, ist eine std::function mit die erste Wahl um ein Lambda zu speichern, wenn es sich zur Laufzeit ändern könnte.

// first create std::function without a default.
std::function<void()> f_null;

// f_null contains a nullptr now!
if( f_null ) { // calls operator bool()
    f_null(); // without the if check this would throw!
}


// now create a std::function with a no-op default.
std::function<void()> f_default( []() {} );
f_default(); // can be called unconditionally! :)

struct Test
{
    // in real world code this would be private ...
    std::function<void()>  mMemFun = [](){};
       
    void Call() const
    {
        mMemFun(); // can call unconditionally.
    }
};

Test test;   // default construct with no-op lambda.
test.Call();
test.mMemFun = []() { puts( "Test from TOP No. 5." ); };
test.Call();

Nummer 4

explicit Constructor( X x, Y y, Z z )
try : mMember1( x ) 
    , mMember12( y, z )
{
} catch( ... ) {
}

Dies ist ein „Function Try-Block„. Der Try-Block schließt die Initializer-Liste mit ein.

Dies ist nützlich, wenn in der Mitte der Member-Initialisation eine Exception geworfen wird und die halb erzeugte Klasseninstanz Aufräumarbeiten erledigen muss. Dieses Konstrukt hat ein Spezialverhalten. Weißt du welches?

Der Catch-Block wird die die Exception in jedem Fall automatisch weiter werfen, da die Klasseninstanz nicht final erzeugt wurde und defekt bleibt.

class Foo
{
    std::string mStr;
public:
    // Constructor
    explicit Foo( std::string const & s, size_t pos )
    // # 4: Function try block. The try block includes the initializer list!
    try : mStr( s, pos ) 
    {
        puts( "Foo constructor - normal block." );
    } catch( ... ) {
        // here can only cleanup already initialized/allocated stuff, which must be clean up manually. 
        // Constructed members are destructed already.
        // the class instance itself stays broken. this catch block will auto re-throw the exception always!
        puts( "Foo constructor - catch block!." );
    }
};

try {
    std::string s = "test";
    Foo foo1( s, 2 );  // not throw
    Foo foo2( s, 10 ); // will throw std::out_of_range
} catch( ... ) {
}

Nummer 3

template<typename T>
explicit Constructor( long long x ) noexcept(noexcept(T(x)))
// ...

Das noexcept( noexcept() ). Das innere noexcept ist der Noexcept-Operator, das äußere der Noexcept-Modifikator (engl. specifier).

Dieses Konstrukt ist nützlich, wenn die Noexcept-Spezifikation von einer Template-Spezialisierung abhängt und mal werfen kann und mal nicht.

class Throw
{
    long long z = 23;
public:
    // this constructor might throw
    explicit Throw( long long x )
        : z( x )
    {
        // just pick some condition when throw...
        if( x % 2 ) throw std::runtime_error( "Throw" );
    }
};

class NoThrow
{
    long long a = 42;
public:
    // this constructor is noexcept - never throws
    NoThrow( long long x ) noexcept
        : a(x)
    {
    }
};

template< class T>
class Foo
{
    T mT;
public:
    // # 3: the noexcept( noexcept() ), the inner noexcept is the noexcept operator, the outer is the noexcept specifier.
    explicit Foo( long long x ) noexcept(noexcept(T(x)))
        : mT(x)
    {
        std::cout << "Foo is noexcept " << std::boolalpha << noexcept(Foo( x )) << std::noboolalpha << std::endl;
    }
};


Foo<Throw>    foo1(4LL);     // foo1 may throw....
Foo<NoThrow>  foo2(101LL);   // foo2 never throws.

Nummer 2

template<typename T>
T mul( T const & t ) requires requires { t * t; }
{ /* ... */ }

Das requires requires. Das erste ist das Require-Constraint, das zweite ein ad-hoc Require-Ausdruck (anstatt ein bereits vorhandenes Konzept zu verwenden).

Damit kann man ein ad-hoc Constraint spezifizieren anstatt ein Konzept anzugeben. Eine Methode einer Klasse könnte nur für bestimmte Template-Spezialisierungen zur Verfügung stehen.

template< class T >
class Bar
{
    T mT;
public:
    Bar( T t )
        : mT(t)
    { }

    // # 2: requires requires, first is the requires constraint, second the ad-hoc requires expression (instead of an already usable concept)
    T mul( T const & t ) requires requires { t * t; }
    {
        // t has the binary operator*()
        return mT * t;
    }
};


Bar bar1( 2 ); // T is int
std::cout << "bar1.mul(3) == " << bar1.mul( 3 ) << std::endl; // compiles, int has * operator
Bar bar2( std::string("test") ); // now T is std::string
// will not compile, std::string has no operator *
// error C7500: 'mul': no function satisfied its constraints
//std::cout << "bar2.mul(test 2) == " << bar2.mul( std::string("test 2") ) << std::endl;

Und jetzt …

… die …

ultimative und erstaunlichste

Nummer 1

auto res = lambda_value.operator()< int >( 1LL );

Sieh genau hin. Dort sind zuerst runde Klammern, dann spitze Klammern und schließlich wieder runde Klammern.

Ich bewerte dieses Beispiel als erstaunlichste Syntax, da ich selbst auf diese in meinem eigenem „real-world“ Code gestoßen bin. Der interessante Teil ist, dass sich dahinter eine Lambda-Funktion verbirgt und es nur funktioniert, wenn man es auf diese Weise aufruft – ja, eine Lambda-Funktion. Vor meiner Entdeckung konnte ich mir nicht vorstellen, dass man eine Lambda-Funktion auf diese Art und Weise aufrufen kann und sogar muss.

Dieses Konstrukt ist notwendig, wenn man z.B. einen expliziten Return-Type angeben will und dieser von Aufruf zu Aufruf unterschiedlich sein kann.

Dann könnte ein Lambda-Konstrukt und der Aufruf folgendermaßen aussehen:

// first construct the lambda. (The construct itself could also be judged as No. 1 astounding syntax, couldn't it? ;-) )
auto lambda_value = []<typename RET>( auto n ) noexcept
{
    if( n < std::numeric_limits<RET>::min() ) {
        return std::numeric_limits<RET>::min();
    } else if( n > std::numeric_limits<RET>::max() ) {
        return std::numeric_limits<RET>::max();
    } else {
        return static_cast<RET>(n);
    }
};

// # 1: explicit template specialization of the operator () of a lambda!
auto res = lambda_value.operator()< int >( 1LL ); // param is long long, return type is explicit set to int.
std::cout << "lambda_value.operator()< int >( 1LL ) == " << res << std::endl;
auto res2 = lambda_value.operator()< signed char >( 129LL ); // param is long long, return type is explicit set to signed char.
std::cout << "lambda_value.operator()< signed char >(129LL) == " << static_cast<int>(res2) << std::endl;

Mein „real-world“ Beispiel ist von einem Unittest von TeaScript.

Dort gibt es auch Konstrukte, die unterschiedliche Return-Typen haben (z.B. Return-Werte von evaluierten TeaScript Ausdrücken) und ich musste wiederverwendbaren Code kapseln um das Programmieren zu vereinfachen, die Lesbarkeit zu erhöhen und schneller Fortschritt zu erzielen.

auto eval_value = []<typename Ret>( teascript::Parser &p, teascript::Context &c, auto content ) {
        p.ClearState();
        return p.Parse( content, "_TEST_" )->Eval( c ).GetValue<Ret>();
    };

// and then I used it like this:
BOOST_CHECK_THROW( eval_value.operator()<long long>(p, c, "a := b + d"), teascript::exception::unknown_identifier );

// or like that
BOOST_CHECK_EQUAL( eval_value.operator()< bool >(p, c, "undef variable"), true );
// and so on ...

Das waren meine TOP 5 der erstaunlichsten C++20 Syntax Konstrukte.

Alle Beispiele stehen auf godbolt bereit zum ausprobieren: https://godbolt.org/z/1TEnzx8x1

Kennst du noch weitere erstaunliche Konstrukte?
Welches mögliche und gültige (und nützliche?) C++20 Konstrukt ist deine persönliche Nummer 1?

Ich möchte mein Weihnacht-Spezial mit einer kurzen und spaßigen Exkursion beenden.

{({()}),({()})}

OK, das ist kein gültiges C++, aber zurzeit gültiger (aber nutzloser, schätze ich) TeaScript Syntax.

Weißt du, was dabei herauskommt?

Mit dieser Aufgabe lasse ich dich jetzt die Weihnachtszeit genießen.

Vielleicht möchtest du während deiner Ferien/deines Urlaubs ein wenig mit TeaScript experimentieren und spielen, insbesondere mit den bereits verfügbaren Functional Programming Fähigkeiten. Keine Installation erforderlich – einfach runterladen und benutzen!

Jedes Feedback ist herzlich willkommen. Ich hoffe, der Blog Post hat dir gefallen.

Ich wünsche dir Frohe Weihnachten 🎄🎅🎁 und ein Frohes Neues Jahr 2023 !!! 🎉🎆🎉🎆🎉🎆

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert