Der Fluch der variadischen Templates mit Exceptions...oder der Blödheit?



  • Tach 😃

    Ich hab mal folgendes gemacht:

    #include <windows.h>
    #include <stdexcept>
    #include <iostream>
    #include <string>
    #include <sstream>
    
    template<typename... Ts> auto ArgumentExpander(Ts&&... ts) -> std::string
    {
        std::ostringstream oss;
    
        int temp[] = {0,(static_cast<void>(oss << ts), 0)...};
    
        static_cast<void>(temp);
    
        return(oss.str());
    }
    
    class C: public std::runtime_error
    {
    public:
        template<typename... T> C(T&&... t);
    
    public:
        virtual ~C() = default;
    };
    
    template<typename... Ts>C::C(Ts&&... ts):
        std::runtime_error(ArgumentExpander(std::forward<Ts>(ts)...)) {}
    
    int WINAPI WinMain(
                    HINSTANCE /**/,
                    HINSTANCE /**/,
                    LPSTR /**/,
                    int /**/)
    {
        C c(1, "aaaaaaaaa", 3 ,"nnnn");
    
        return(0);
    }
    

    Das läßt sich kompilieren und ausführen. Da C von std::runtime_error abgeleitet ist so allerdings sinnfrei.

    int WINAPI WinMain(
                    HINSTANCE /**/,
                    HINSTANCE /**/,
                    LPSTR /**/,
                    int /**/)
    {
        try{
            throw C(1, "aaaaaaaaa", 3 ,"nnnn");
        }
        catch(const C& ex)
        {
            std::cout << ex.what();
        }
    
        return(0);
    }
    

    Hier führt das Kompilieren allerdings schon zum Fehler...

    In instantiation of 'std::__cxx11::string ArgumentExpander(Ts&& ...) [with Ts = {C}; std::__cxx11::string = std::__cxx11::basic_string<char>]':
    
    required from 'C::C(Ts&& ...) [with T = {C}]'
    

    Das Problem ist der folgende Aufruf throw C(1, "aaaaaaaaa", 3 ,"nnnn");

    Kommentiere ich explicit aus, dann läßt es sich kompilieren und ausführen.

    Ich benutze hier ein variadic Template (enthält mindestens einen Parameter Pack). In der Funktion ArgumentExpander wird der Pack ausgepackt.
    Ich dachte bis jetzt das folgendes gemacht wird:

    'std::__cxx11::string ArgumentExpander(Ts&& ...) [with Ts = {int, const char (&)[10], int, const char (&)[5]}
    

    TS wird also nach und nach ausgepackt...in exakt genau fünf Parameter.
    In der Funktion ArgumentExpander wird das temp array idiom dazu "missbraucht" die stream operator für jedes einzelne t im Pack aufzurufen.
    Das Problem ist das der ostringstream operator nicht auf die Klasse C (ist wahrscheinlich das this) angewandt werden kann.
    Wieso muss explicit im Konstruktor stehen...und wieso vermeidet das den Fehler...with T = {C}]'

    edit:

    Das Problem taucht auf beim mingw

    x86_64-w64-mingw32-g++.exe -Wnon-virtual-dtor -Wshadow -Winit-self -Wredundant-decls -Wcast-align -Wundef -Wfloat-equal -Winline -Wunreachable-code -Wmissing-declarations -Wmissing-include-dirs -Wswitch-enum -Wswitch-default -Weffc++ -Wzero-as-null-pointer-constant -pedantic-errors -pedantic -Wfatal-errors -Wextra -Wall -std=c++14 -DUNICODE -D_UNICODE
    

    Dem Visual Studio 2013 ist das wurscht (mit Warning Level: W4)

    Gruß



  • Füg mal noch

    C(C&& c) : std::runtime_error{ std::move(c) } { }
    

    ein und gut ist.



  • Jo, iss gut 👍

    Magst du mich kurz erleuchten, warum der Move-Konstruktor das Explicit beim "Template-Konstruktor" überflüssig macht und dann auch der Fehler (required from...) nicht mehr auftritt. Das Visual Studio hat sich komischerweise darüber nie aufgeregt.

    Danke 🙂



  • Also erstmal... damit du was werfen kannst muss es einen copy- oder move-ctor haben.

    FrankTheFox schrieb:

    Kommentiere ich explicit aus, dann läßt es sich kompilieren und ausführen.

    Nein, umgekehrt. Wenn du den template-ctor explicit machst läßt es sich kompilieren. Denn dann passt er nicht mehr als Vorlage für einen copy- oder move-ctor, da diese nicht explicit sein können. Und da er nicht mehr passt, und es auch sonst keinen passenden userdefinierten ctor gibt, bekommst du einen compilergenerierten. Und der funktioniert.

    Wenn der template-ctor dagegen nicht explicit ist, dann gilt er als Vorlage, und der Compiler versucht daraus einen move-ctor zu erstellen. Was nicht geht, da dein Template versucht alle ctor-Argumente über ArgumentExpander in einen Stream zu stopfen. Was mit C nicht geht, weil es den passenden operator nicht gibt.

    VS 2013 ist es vermutlich einfach deswegen wurst, weil VS 2013 da nen Bug hat. VS 2017 verweigert deinen Code ohne explicit genau so zu compilieren wie Clang oder GCC.

    Die Lösung von Jodocus ist übrigens auch nur ne halbe Sache. Das funktioniert zwar in deinem Beispiel, aber in anderen Situationen, z.B. wenn ein copy-ctor für ein benanntes Objekt gesucht wird, wird wieder der selbe Fehler kommen. Siehe dazu z.B.:
    https://mpark.github.io/programming/2014/06/07/beware-of-perfect-forwarding-constructors/



  • Die Testklasse heißt mal B und fn ist der ArgumentExpander.

    class B: public std::runtime_error
    {
    public:
    
        B(): std::runtime_error("Unknown error"){}
        virtual ~B() = default;
    
        template<class ... Args,std::enable_if_t<(sizeof...(Args)>1), bool> = true>
        B(Args &&...args): std::runtime_error(fn(std::forward<Args>(args)...))
        {
            std::cout<<"ctor1: ";
        }
    
        template<class Arg,std::enable_if_t<!std::is_base_of<B, std::remove_reference_t<Arg>>::value, bool> = true>
        B(Arg && arg): std::runtime_error(fn(std::forward<Arg>(arg)))
        {
            std::cout<<"ctor2: ";
        }
    
        B(const B &b): std::runtime_error(b.what())
        {
            std::cout<<"ctor3: ";
        }
    
        B(B && b): std::runtime_error(std::move(b))
        {
            std::cout<<"ctor4: ";
        }
    };
    

    hustbaer schrieb:

    Also erstmal... damit du was werfen kannst muss es einen copy- oder move-ctor haben.

    Den habe ich jetzt 😉

    hustbaer schrieb:

    Nein, umgekehrt. Wenn du den template-ctor explicit machst läßt es sich kompilieren....

    ja..das hatte ich gemeint...nur nicht geschrieben.

    hustbaer schrieb:

    VS 2013 ist es vermutlich einfach deswegen wurst, weil VS 2013 da nen Bug hat. VS 2017 verweigert deinen Code ohne explicit genau so zu compilieren wie Clang oder GCC.

    dann sollte jetzt auch VS2017 nicht meckern.

    Die Tests...

    try{
            int a{1};
            std::string s{"ddddddd"};
    
            //throw B(s, a);
            //throw B(s);
            //throw B("aaa", 1,2, "ccc");
            throw B("aaaaaaaaaaaaaaaaaaaaaaa");
        }
        //catch(const B& b)
        catch(B b)
        {
            std::cout << b.what();
        }
    

    scheinen aber zu funktionieren, nur was für ein Aufriss 😮

    vielleicht sollte ich mal...

    try{
       //...
       throw std::runtime_error(fn("aaaa","bbbb",23));
    }
    catch(const std::runtime_error& ex){
       std::cout << ex.what() << std::endl;
    }
    

    ins Auge fassen. Ist halt nur keine so schöne eigene Exceptionklasse.

    Gruß



  • Ich würde den Ctor einfach explicit machen. Ich mach' sowieso grundsätzlich alle ctor explicit die ich nicht wirklich implicit brauche. Was spricht da dagegen?