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.
variadic constructor are dangerous, they match also (not const) copy constructor.
Adding that constructor solves the issue
CLASS_NAME(CLASS_NAME&) = default; \
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.