23
\$\begingroup\$

Today, I decided to implement std::any using the cppreference page. I've never actually used std::any before and after seeing the implementation first hand... I don't think I'll start now! I'm not entirely sure what this class is actually meant for. I'm not even sure why I implemented this in the first place...

Anyway, here's the code:

#include <memory>
#include <utility>
#include <typeinfo>

namespace mystd {

template <typename T>
struct is_in_place_type : std::false_type {};

template <typename T>
struct is_in_place_type<std::in_place_type_t<T>> : std::true_type {};

class any {
  template <typename ValueType>
  friend const ValueType *any_cast(const any *) noexcept;

  template <typename ValueType>
  friend ValueType *any_cast(any *) noexcept;

public:
  // constructors

  constexpr any() noexcept = default;

  any(const any &other) {
    if (other.instance) {
      instance = other.instance->clone();
    }
  }

  any(any &&other) noexcept
    : instance(std::move(other.instance)) {}

  template <typename ValueType, typename = std::enable_if_t<
    !std::is_same_v<std::decay_t<ValueType>, any> &&
    !is_in_place_type<std::decay_t<ValueType>>::value &&
    std::is_copy_constructible_v<std::decay_t<ValueType>>
  >>
  any(ValueType &&value) {
    static_assert(std::is_copy_constructible_v<std::decay_t<ValueType>>, "program is ill-formed");
    emplace<std::decay_t<ValueType>>(std::forward<ValueType>(value));
  }

  template <typename ValueType, typename... Args, typename = std::enable_if_t<
    std::is_constructible_v<std::decay_t<ValueType>, Args...> &&
    std::is_copy_constructible_v<std::decay_t<ValueType>>
  >>
  explicit any(std::in_place_type_t<ValueType>, Args &&... args) {
    emplace<std::decay_t<ValueType>>(std::forward<Args>(args)...);
  }

  template <typename ValueType, typename List, typename... Args, typename = std::enable_if_t<
    std::is_constructible_v<std::decay_t<ValueType>, std::initializer_list<List> &, Args...> &&
    std::is_copy_constructible_v<std::decay_t<ValueType>>
  >>
  explicit any(std::in_place_type_t<ValueType>, std::initializer_list<List> list, Args &&... args) {
    emplace<std::decay_t<ValueType>>(list, std::forward<Args>(args)...);
  }

  // assignment operators

  any &operator=(const any &rhs) {
    any(rhs).swap(*this);
    return *this;
  }

  any &operator=(any &&rhs) noexcept {
    any(std::move(rhs)).swap(*this);
    return *this;
  }

  template <typename ValueType>
  std::enable_if_t<
    !std::is_same_v<std::decay_t<ValueType>, any> &&
    std::is_copy_constructible_v<std::decay_t<ValueType>>,
    any &
  >
  operator=(ValueType &&rhs) {
    any(std::forward<ValueType>(rhs)).swap(*this);
    return *this;
  }

  // modifiers

  template <typename ValueType, typename... Args>
  std::enable_if_t<
    std::is_constructible_v<std::decay_t<ValueType>, Args...> &&
    std::is_copy_constructible_v<std::decay_t<ValueType>>,
    std::decay_t<ValueType> &
  >
  emplace(Args &&... args) {
    auto new_inst = std::make_unique<storage_impl<std::decay_t<ValueType>>>(std::forward<Args>(args)...);
    std::decay_t<ValueType> &value = new_inst->value;
    instance = std::move(new_inst);
    return value;
  }

  template <typename ValueType, typename List, typename... Args>
  std::enable_if_t<
    std::is_constructible_v<std::decay_t<ValueType>, std::initializer_list<List> &, Args...> &&
    std::is_copy_constructible_v<std::decay_t<ValueType>>,
    std::decay_t<ValueType> &
  >
  emplace(std::initializer_list<List> list, Args &&... args) {
    auto new_inst = std::make_unique<storage_impl<std::decay_t<ValueType>>>(list, std::forward<Args>(args)...);
    std::decay_t<ValueType> &value = new_inst->value;
    instance = std::move(new_inst);
    return value;
  }

  void reset() noexcept {
    instance.reset();
  }

  void swap(any &other) noexcept {
    std::swap(instance, other.instance);
  }

  // observers

  bool has_value() const noexcept {
    return static_cast<bool>(instance);
  }

  const std::type_info &type() const noexcept {
    return instance ? instance->type() : typeid(void);
  }

private:
  struct storage_base;

  std::unique_ptr<storage_base> instance;

  struct storage_base {
    virtual ~storage_base() = default;

    virtual const std::type_info &type() const noexcept = 0;
    virtual std::unique_ptr<storage_base> clone() const = 0;
  };

  template <typename ValueType>
  struct storage_impl final : public storage_base {
    template <typename... Args>
    storage_impl(Args &&... args)
      : value(std::forward<Args>(args)...) {}

    const std::type_info &type() const noexcept override {
      return typeid(ValueType);
    }

    std::unique_ptr<storage_base> clone() const override {
      return std::make_unique<storage_impl<ValueType>>(value);
    }

    ValueType value;
  };
};

} // mystd

template <>
void std::swap(mystd::any &lhs, mystd::any &rhs) noexcept {
  lhs.swap(rhs);
}

namespace mystd {

class bad_any_cast : public std::exception {
public:
  const char *what() const noexcept {
    return "bad any cast";
  }
};

// C++20
template <typename T>
using remove_cvref_t = std::remove_cv_t<std::remove_reference_t<T>>;

// any_cast

template <typename ValueType>
ValueType any_cast(const any &anything) {
  using value_type_cvref = remove_cvref_t<ValueType>;
  static_assert(std::is_constructible_v<ValueType, const value_type_cvref &>, "program is ill-formed");
  if (auto *value = any_cast<value_type_cvref>(&anything)) {
    return static_cast<ValueType>(*value);
  } else {
    throw bad_any_cast();
  }
}

template <typename ValueType>
ValueType any_cast(any &anything) {
  using value_type_cvref = remove_cvref_t<ValueType>;
  static_assert(std::is_constructible_v<ValueType, value_type_cvref &>, "program is ill-formed");
  if (auto *value = any_cast<value_type_cvref>(&anything)) {
    return static_cast<ValueType>(*value);
  } else {
    throw bad_any_cast();
  }
}

template <typename ValueType>
ValueType any_cast(any &&anything) {
  using value_type_cvref = remove_cvref_t<ValueType>;
  static_assert(std::is_constructible_v<ValueType, value_type_cvref>, "program is ill-formed");
  if (auto *value = any_cast<value_type_cvref>(&anything)) {
    return static_cast<ValueType>(std::move(*value));
  } else {
    throw bad_any_cast();
  }
}

template <typename ValueType>
const ValueType *any_cast(const any *anything) noexcept {
  if (!anything) return nullptr;
  auto *storage = dynamic_cast<any::storage_impl<ValueType> *>(anything->instance.get());
  if (!storage) return nullptr;
  return &storage->value;
}

template <typename ValueType>
ValueType *any_cast(any *anything) noexcept {
  return const_cast<ValueType *>(any_cast<ValueType>(static_cast<const any *>(anything)));
}

// make_any

template <typename ValueType, typename... Args>
any make_any(Args &&... args) {
  return any(std::in_place_type<ValueType>, std::forward<Args>(args)...);
}

template <typename ValueType, typename List, typename... Args>
any make_any(std::initializer_list<List> list, Args &&... args) {
  return any(std::in_place_type<ValueType>, list, std::forward<Args>(args)...);
}

} // mystd

I'm thinking about doing this in C++11 without rigorously adhering to the standard and without RTTI. Maybe another day...

\$\endgroup\$
3
  • 4
    \$\begingroup\$ There's nothing much to say except it's excellent. As to why use std::any, I'd say it's rarely useful, because if you know the possible type values you'd use std::variant, and if not you'll be embarrassed to cast it back to a usable value / pointer. Besides, C++ programmers are used to avoid RTTI and Java-like hierarchies under a very abstract Object type. Nonetheless it can find a use in evolutive / pluggable / distributed programs, where different components can try and recognize what a std::any really is. \$\endgroup\$
    – papagaga
    Commented Apr 25, 2019 at 8:07
  • \$\begingroup\$ In any_cast of a pointer, you check if the pointer is null before dynamic_cast, and then again after. But dynamic_cast<T>(nullptr) just gives back a null pointer of type T (en.cppreference.com/w/cpp/language/dynamic_cast item 2), so these two checks can be merged. Is there a reason to keep them separate, such as clarity or optimization? \$\endgroup\$
    – Riley
    Commented Oct 7, 2021 at 12:20
  • 1
    \$\begingroup\$ @Riley I’m not checking anything and casting anything. I’m checking anything and casting anything->storage. If either of the null checks are removed then I’d be dereferencing a null pointer. \$\endgroup\$ Commented Oct 7, 2021 at 21:09

2 Answers 2

14
\$\begingroup\$

Your implementation is excellent! I can hardly find any problems. I was amazed how simple a conforming implementation of any can be. And I wholeheartedly agree with @papagaga's comment.

Here's my two cents. I use the N4659, the C++17 final draft, as a reference.

Non-conformance (priority: high)

  1. Thou Shalt Not Specialize std::swap. Instead, you should overload swap to be found by ADL. See How to overload std::swap() on Stack Overflow.

    class any {
    public:
      // ...
      friend void swap(any& lhs, any& rhs)
      {
        lhs.swap(rhs);
      }
    };
    
  2. [any.bad_any_cast]/2 specifies that bad_any_cast should derive from std::bad_cast. Your implementation fails to do this.

Other suggestions (priority: low)

  1. [any.class]/3 says:

    Implementations should avoid the use of dynamically allocated memory for a small contained value. [ Example: where the object constructed is holding only an int. — end example ] Such small-object optimization shall only be applied to types T for which is_nothrow_move_constructible_v<T> is true.

    Clearly, you did not implement this optimization.

  2. Initially I thought, "where is your destructor?" Then I realized that the synthesized destructor is equivalent to reset(). I recommend you explicitly default this to reduce confusion since you implemented the rest of the Big Five.

    ~any() = default;
    
  3. The following static_assert on line 40 is unnecessary:

    static_assert(std::is_copy_constructible_v<std::decay_t<ValueType>>, "program is ill-formed");
    

    because this constructor does not participate in overload resolution unless std::is_copy_constructible_v<std::decay_t<ValueType>>.

\$\endgroup\$
2
  • 2
    \$\begingroup\$ I was hoping for a review that mentioned nonconformance! Specializing the swap template felt a bit weird. I overloaded swap in the past but I guess just I forgot this time. The cppreference page does say that bad_any_cast derives from bad_cast so my fault for not reading carefully. I've never implemented SBO before so I wasn't sure how to do it. I do remember explicitly defaulting the destructor at some point but I guess I deleted it when reordering things (oops). I knew the static_assert was redundant but the cppreference page mentions "program is ill-formed" so I did it anyway! Great review! \$\endgroup\$ Commented Apr 27, 2019 at 4:45
  • \$\begingroup\$ @Kerndog73 Thank you! Hope you don't liberally forget things in the future ;-) About SBO: it's not that hard. You can just specialize the template for types that meet some criteria (e.g., sizeof(T) is less than some threshold) and implement it accordingly. \$\endgroup\$
    – L. F.
    Commented Apr 27, 2019 at 4:47
0
\$\begingroup\$

It is good exercise to try to implement std features yourself. But if you want an elegant implementation of any, check boost::any. It has a very readable code and implements both: small value optimization & RTTI without built-in support(i.e. not using virtual nor typeid keywords). More interestingly, there is a boost::anys::basic_any template class who enables you to decide how big the small values to optimize can be.

regards,

FM.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.