On VS2015 and VS2017, this compiles with no warning, and generates an access violation that cannot be caught and crashes the application. Obviously the int 0 is silently con
namespace safer {
template<class CharT,
class Traits = ::std::char_traits<CharT>,
class Allocator = ::std::allocator<CharT>,
class Base = ::std::basic_string<CharT, Traits, Allocator>
>
struct basic_string:
Base
{
using Base::Base;
basic_string( CharT const* ptr ):
Base( ptr?Base(ptr):Base() )
{}
};
using string = basic_string<char>;
using wstring = basic_string<wchar_t>;
}
This safer::string
is basically identical to std::string
but doesn't crash when constructed from a null pointer. Instead, it treats it as an empty string.
Simply sweep all mention of std::string
from your code base and replace with safer::string
, and similar for std::wstring
and std::basic_string
.
void crash(const safer::string& s) {}
You could choose to throw rather than silently consume the value.
We can detect 0
at compile time as well:
namespace safer {
template<class CharT,
class Traits = ::std::char_traits<CharT>,
class Allocator = ::std::allocator<CharT>,
class Base = ::std::basic_string<CharT, Traits, Allocator>
>
struct basic_string:
Base
{
using Base::Base;
basic_string( CharT const* ptr ):
Base( ptr?Base(ptr):Base() )
{}
template<class T,
// SFINAE only accepts prvalues of type int:
std::enable_if_t<std::is_same<T, int>::value, bool> = true
>
basic_string(T&&)=delete; // block 0
};
using string = basic_string<char>;
using wstring = basic_string<wchar_t>;
}
and now passing 0
gets a compile-time error, passing nullptr
or a null char const*
gets you an empty string.
live example.
So, there are people who get nervous about the fact I told you to inherit from a non-polymorphic type in std
. There are reasons not to inherit from a non-polymorphic type, but none of them apply here.
In general, however, be careful when inheriting from types not designed for polymorphism like std::basic_string<CharT>
. In this particular case, storing a safer::basic_string<T>
in a std::basic_string<T>*
and then calling delete
on it is undefined behavior (or in a std::unique_ptr<std::basic_string<T>>
which calls delete
). But dynamically allocating basic_string
s is usually a mistake in the first place, so that is unlikely to occur.
In addition, this inheritance has to obey the LSP without changing the behavior of any method of the base class. In this case we are adapting construction, and construction is never polymorphic. If we had any write operations that were not construction where we wanted to maintain an invariant in the descended class we would be in trouble.
For this specific case, you can get a compile time error by using C++11 std::nullptr_t
, just add the following deleted overload:
void crash(std::nullptr_t) = delete;
Of course, this won't protect you against passing null (or non-null-terminated ) char* pointers ... you are violating an std::string constructor precondition, resulting in undefined behavior; this is by definition unrecoverable.
Alternatively, if you really need to catch these errors at runtime in a possibly recoverable way, you could write a const char*
overload that throws if given a null pointer or invokes the std::string const&
version otherwise.
If your real function takes more than a few string arguments, and overloading all possible combinations seems not feasible, you could resort writing a function template, performing all the checks over the deduced types ex-post.