C++ is a reasonable language for reasonable people
Simon Brand
@TartanLlama
In C++, you can introduce UB by changing where you write =default
struct foo {� foo() = default;� int a;�};��struct bar {� bar();� int b;�};��bar::bar() = default;��int main() {� foo a{};� bar b{};� std::cout << a.a //okay
<< b.b; //UB�}
In C++, this is valid code
class A {�public:� virtual void foo() = 0;�};��class B : public A {�public:� virtual void foo();�};��void B::B::B::B::B::B::B::B::B::B::B::� B::B::B::B::B::B::B::B::B::B::� B::A::foo(){}
In C++, you get a cache miss on pretty much every lookup in the STL hash table
In C++, you can store state at compile time by defining friend functions
constexpr int flag (int);�template<class Tag>�struct writer {� friend constexpr int flag (Tag) {� return 0;� }�};�template<bool B, class Tag = int>�struct dependent_writer : writer<Tag> { };�template<� bool B = noexcept (flag (0)),� int = sizeof (dependent_writer<B>)�>�constexpr int f () {� return B;�}�int main () {� constexpr int a = f ();� constexpr int b = f ();�� static_assert (a != b, "fail");�}
In C++, sum types are footguns
std::variant<std::string, bool> lol = "oh no";�std::visit([](auto&& x) { std::cout << x; }, lol);
In C++, non-allocating strings are footguns
std::string s = "Hellooooooooooooooo ";�std::string_view sv = s + "World\n";�std::cout << sv;
In C++, my std::expected assignment operator is hundreds of lines of template nonsense
// This class manages conditionally having a trivial copy assignment operator�template <class T, class E,� bool = is_void_or<� T, conjunction<TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(T),� TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(T),� TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(T)>>::value� &&TL_EXPECTED_IS_TRIVIALLY_COPY_ASSIGNABLE(E)::value� &&TL_EXPECTED_IS_TRIVIALLY_COPY_CONSTRUCTIBLE(E)::value� &&TL_EXPECTED_IS_TRIVIALLY_DESTRUCTIBLE(E)::value>�struct expected_copy_assign_base : expected_move_base<T, E> {� using expected_move_base<T, E>::expected_move_base;�};��template <class T, class E>�struct expected_copy_assign_base<T, E, false> : expected_move_base<T, E> {� using expected_move_base<T, E>::expected_move_base;�� expected_copy_assign_base() = default;� expected_copy_assign_base(const expected_copy_assign_base &rhs) = default;�� expected_copy_assign_base(expected_copy_assign_base &&rhs) = default;� expected_copy_assign_base &operator=(const expected_copy_assign_base &rhs) {� this->assign(rhs);� return *this;� }� expected_copy_assign_base &� operator=(expected_copy_assign_base &&rhs) = default;�};��// This class manages conditionally having a trivial move assignment operator�// Unfortunately there's no way to achieve this in GCC < 5 AFAIK, since it�// doesn't implement an analogue to std::is_trivially_move_assignable. We have�// to make do with a non-trivial move assignment operator even if T is trivially�// move assignable�
#ifndef TL_EXPECTED_GCC49�template <class T, class E,� bool =� is_void_or<T, conjunction<std::is_trivially_destructible<T>,� std::is_trivially_move_constructible<T>,� std::is_trivially_move_assignable<T>>>::� value &&std::is_trivially_destructible<E>::value� &&std::is_trivially_move_constructible<E>::value� &&std::is_trivially_move_assignable<E>::value>�struct expected_move_assign_base : expected_copy_assign_base<T, E> {� using expected_copy_assign_base<T, E>::expected_copy_assign_base;�};
�#else�template <class T, class E, bool = false> struct expected_move_assign_base;�#endif��template <class T, class E>�struct expected_move_assign_base<T, E, false>� : expected_copy_assign_base<T, E> {� using expected_copy_assign_base<T, E>::expected_copy_assign_base;�� expected_move_assign_base() = default;� expected_move_assign_base(const expected_move_assign_base &rhs) = default;�� expected_move_assign_base(expected_move_assign_base &&rhs) = default;�� expected_move_assign_base &� operator=(const expected_move_assign_base &rhs) = default;�� expected_move_assign_base &� operator=(expected_move_assign_base &&rhs) noexcept(� std::is_nothrow_move_constructible<T>::value� &&std::is_nothrow_move_assignable<T>::value) {� this->assign(std::move(rhs));� return *this;� }�};�
// expected_delete_assign_base will conditionally delete copy and move�// constructors depending on whether T and E are copy/move constructible +�// assignable�template <class T, class E,� bool EnableCopy = (std::is_copy_constructible<T>::value &&� std::is_copy_constructible<E>::value &&� std::is_copy_assignable<T>::value &&� std::is_copy_assignable<E>::value),� bool EnableMove = (std::is_move_constructible<T>::value &&� std::is_move_constructible<E>::value &&� std::is_move_assignable<T>::value &&� std::is_move_assignable<E>::value)>�struct expected_delete_assign_base {� expected_delete_assign_base() = default;� expected_delete_assign_base(const expected_delete_assign_base &) = default;� expected_delete_assign_base(expected_delete_assign_base &&) noexcept =� default;� expected_delete_assign_base &� operator=(const expected_delete_assign_base &) = default;� expected_delete_assign_base &� operator=(expected_delete_assign_base &&) noexcept = default;�};��template <class T, class E>�struct expected_delete_assign_base<T, E, true, false> {� expected_delete_assign_base() = default;� expected_delete_assign_base(const expected_delete_assign_base &) = default;� expected_delete_assign_base(expected_delete_assign_base &&) noexcept =� default;� expected_delete_assign_base &� operator=(const expected_delete_assign_base &) = default;� expected_delete_assign_base &� operator=(expected_delete_assign_base &&) noexcept = delete;�};�
template <class T, class E>�struct expected_delete_assign_base<T, E, false, true> {� expected_delete_assign_base() = default;� expected_delete_assign_base(const expected_delete_assign_base &) = default;� expected_delete_assign_base(expected_delete_assign_base &&) noexcept =� default;� expected_delete_assign_base &� operator=(const expected_delete_assign_base &) = delete;� expected_delete_assign_base &� operator=(expected_delete_assign_base &&) noexcept = default;�};��template <class T, class E>�struct expected_delete_assign_base<T, E, false, false> {� expected_delete_assign_base() = default;� expected_delete_assign_base(const expected_delete_assign_base &) = default;� expected_delete_assign_base(expected_delete_assign_base &&) noexcept =� default;� expected_delete_assign_base &� operator=(const expected_delete_assign_base &) = delete;� expected_delete_assign_base &� operator=(expected_delete_assign_base &&) noexcept = delete;�};��emplate <� class U = T, class G = T,� detail::enable_if_t<std::is_nothrow_constructible<T, U &&>::value> * =� nullptr,� detail::enable_if_t<!std::is_void<G>::value> * = nullptr,� detail::enable_if_t<� (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&� !detail::conjunction<std::is_scalar<T>,� std::is_same<T, detail::decay_t<U>>>::value &&� std::is_constructible<T, U>::value &&� std::is_assignable<G &, U>::value &&� std::is_nothrow_move_constructible<E>::value)> * = nullptr>� expected &operator=(U &&v) {� if (has_value()) {� val() = std::forward<U>(v);� } else {� err().~unexpected<E>();� ::new (valptr()) T(std::forward<U>(v));� this->m_has_val = true;� }�� return *this;� }��
/// \exclude� template <� class U = T, class G = T,� detail::enable_if_t<!std::is_nothrow_constructible<T, U &&>::value> * =� nullptr,� detail::enable_if_t<!std::is_void<U>::value> * = nullptr,� detail::enable_if_t<� (!std::is_same<expected<T, E>, detail::decay_t<U>>::value &&� !detail::conjunction<std::is_scalar<T>,� std::is_same<T, detail::decay_t<U>>>::value &&� std::is_constructible<T, U>::value &&� std::is_assignable<G &, U>::value &&� std::is_nothrow_move_constructible<E>::value)> * = nullptr>� expected &operator=(U &&v) {� if (has_value()) {� val() = std::forward<U>(v);� } else {� auto tmp = std::move(err());� err().~unexpected<E>();� try {� ::new (valptr()) T(std::move(v));� this->m_has_val = true;� } catch (...) {� err() = std::move(tmp);� throw;� }� }�� return *this;� }�� template <class G = E,� detail::enable_if_t<std::is_nothrow_copy_constructible<G>::value &&� std::is_assignable<G &, G>::value> * = nullptr>� expected &operator=(const unexpected<G> &rhs) {� if (!has_value()) {� err() = rhs;� } else {� val().~T();� ::new (errptr()) unexpected<E>(rhs);� this->m_has_val = false;� }�� return *this;� }���
� template <class G = E,� detail::enable_if_t<std::is_nothrow_move_constructible<G>::value &&� std::is_move_assignable<G>::value> * = nullptr>� expected &operator=(unexpected<G> &&rhs) noexcept {� if (!has_value()) {� err() = std::move(rhs);� } else {� val().~T();� ::new (errptr()) unexpected<E>(std::move(rhs));� this->m_has_val = false;� }�� return *this;� }�� // These assign overloads ensure that the most efficient assignment� // implementation is used while maintaining the strong exception guarantee.� // The problematic case is where rhs has a value, but *this does not.� //� // This overload handles the case where we can just copy-construct `T`� // directly into place without throwing.� template <class U = T,� detail::enable_if_t<std::is_nothrow_copy_constructible<U>::value>� * = nullptr>� void assign(const expected_operations_base &rhs) noexcept {� if (!this->m_has_val && rhs.m_has_val) {� geterr().~unexpected<E>();� construct(rhs.get());� } else {� assign_common(rhs);� }� }
�� // This overload handles the case where we can attempt to create a copy of� // `T`, then no-throw move it into place if the copy was successful.� template <class U = T,� detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&� std::is_nothrow_move_constructible<U>::value>� * = nullptr>� void assign(const expected_operations_base &rhs) noexcept {� if (!this->m_has_val && rhs.m_has_val) {� T tmp = rhs.get();� geterr().~unexpected<E>();� construct(std::move(tmp));� } else {� assign_common(rhs);� }� }�� // This overload is the worst-case, where we have to move-construct the� // unexpected value into temporary storage, then try to copy the T into place.� // If the construction succeeds, then everything is fine, but if it throws,� // then we move the old unexpected value back into place before rethrowing the� // exception.� template <class U = T,� detail::enable_if_t<!std::is_nothrow_copy_constructible<U>::value &&� !std::is_nothrow_move_constructible<U>::value>� * = nullptr>� void assign(const expected_operations_base &rhs) {� if (!this->m_has_val && rhs.m_has_val) {� auto tmp = std::move(geterr());� geterr().~unexpected<E>();�� try {� construct(rhs.get());� } catch (...) {� geterr() = std::move(tmp);� throw;� }� } else {� assign_common(rhs);� }� }��
� // These overloads do the same as above, but for rvalues� template <class U = T,� detail::enable_if_t<std::is_nothrow_move_constructible<U>::value>� * = nullptr>� void assign(expected_operations_base &&rhs) noexcept {� if (!this->m_has_val && rhs.m_has_val) {� geterr().~unexpected<E>();� construct(std::move(rhs).get());� } else {� assign_common(rhs);� }� }�� template <class U = T,
detail::enable_if_t<!std::is_nothrow_move_constructible<U>::value>� * = nullptr>� void assign(expected_operations_base &&rhs) {� if (!this->m_has_val && rhs.m_has_val) {� auto tmp = std::move(geterr());� geterr().~unexpected<E>();� try {� construct(std::move(rhs).get());� } catch (...) {� geterr() = std::move(tmp);� throw;� }� } else {� assign_common(rhs);� }� }�� // The common part of move/copy assigning� template <class Rhs> void assign_common(Rhs &&rhs) {� if (this->m_has_val) {� if (rhs.m_has_val) {� get() = std::forward<Rhs>(rhs).get();� } else {� get().~T();� construct_error(std::forward<Rhs>(rhs).geterr());� }� } else {� if (!rhs.m_has_val) {� geterr() = std::forward<Rhs>(rhs).geterr();� }� }� }
IMPLICIT CONVERSIONS
C++ is a reasonable language for reasonable people
Simon Brand
@TartanLlama