Search code examples
c++visual-c++variadic-templates

C++ with MSVC: Variadic ctor in derived class gets argument list wrong when calling base class ctor. Compiler bug or user error?


While trying to port code to Windows, MSVC threw a compiler error for code which compiles fine on gcc or clang. I tried to create a somewhat minimal reproducible example below. The idea is to group errors by type, and to allow the fmt syntax in the error construction. The base class normally inherits from std::exception, but to make the example smaller, I removed that, and also the inclusion of fmt here. The problem seems to be the variadic ctor in the derived class.

#include <string>
#include <string_view>

/* to get meaningful error messages, exception classes can define a type and
 * inherit from ErrorWithType, which concatenates the type and the passed in
 * error message for what() output */
class ErrorWithType
{
protected:
    std::string m_error;
    
    std::string errorMsg( std::string_view typeStr, std::string error ){
        return std::string{ typeStr } + ": " + error;
    }
    
public:
    /* ctor to be called when no further arguments are provided */
    ErrorWithType(std::string_view typeStr, std::string error) :
        m_error { errorMsg( typeStr, std::move(error) ) }
    {}

    /* ctor for chaining to fmt::format, left here for illustrative purposes,
     * but compiler error occurs elsewhere */
    // template<typename ... Args>
    // ErrorWithType(std::string_view typeStr, std::string fmtStr, Args&& ... args) :
    //  m_error { errorMsg( typeStr, fmt::format(
    //      std::move(fmtStr),
    //      std::forward<Args>(args) ...
    //  ) ) }
    // {}
    
    const char* what() const noexcept {
        return m_error.c_str();
    }
};


/* the error types are created with this macro,
 * which just contains a static string_view member describing the type,
 * and a variadic ctor, which chains to either of the base class ctors. */
#define ERROR_WITH_TYPE(CLASS_NAME, TYPE_STRING) \
class CLASS_NAME : public ErrorWithType { \
private: static constexpr std::string_view m_type { TYPE_STRING }; \
public: \
    template<typename ... Args> \
    CLASS_NAME(Args&& ... args) : \
        ErrorWithType(m_type, std::forward<Args>(args)...) \
    {} \
};

/* the macro allows creating error types succinctly: */
ERROR_WITH_TYPE(RuntimeError, "Runtime error")
ERROR_WITH_TYPE(LogicError, "Logic error")

void throwError(){
    /* and the next line causes the error */
    throw LogicError("This is a logic error.");
}

The above code compiles fine on clang/gcc (godbolt), but not on MSVC. The error that MSVC throws is:

<source>(53): error C2665: 'ErrorWithType::ErrorWithType': no overloaded function could convert all the argument types
<source>(18): note: could be 'ErrorWithType::ErrorWithType(std::string_view,std::string)'
<source>(53): note: 'ErrorWithType::ErrorWithType(std::string_view,std::string)': 
cannot convert argument 2 from 'LogicError' to 'std::string'
<source>(53): note: No user-defined-conversion operator available 
that can perform this conversion, or the operator cannot be called
<source>(53): note: while trying to match the argument list 
'(const std::string_view, LogicError)'
<source>(57): note: see reference to function template instantiation 'LogicError::LogicError<LogicError&>(LogicError &)' being compiled

But I don't get it. Why is it trying to match an argument list of '(const std::string_view, LogicError)'? I cannot find that being represented in the code. But that causes the spurious conversion from 'LogicError' to 'std::string', which appears to be the cause of the compiler error.

I did find a workaround, by removing the base class and putting everything into the macro, but I liked being able to minimise the amount of code in the macro - and I'd like to understand what's going on here.


Solution

  • variadic constructor are dangerous, they match also (not const) copy constructor.

    Adding that constructor solves the issue

    CLASS_NAME(CLASS_NAME&) = default; \
    

    Demo

    SFINAE the constructor would be another solution.

    The behavior difference should be (an allowed) (but not mandatory) copy-elision done by gcc/clang but not by msvc when throwing.