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...
2 Answers 2
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)
Thou Shalt Not Specialize
std::swap
. Instead, you should overloadswap
to be found by ADL. See How to overloadstd::swap()
on Stack Overflow.class any { public: // ... friend void swap(any& lhs, any& rhs) { lhs.swap(rhs); } };
[any.bad_any_cast]/2 specifies that
bad_any_cast
should derive fromstd::bad_cast
. Your implementation fails to do this.
Other suggestions (priority: low)
[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 typesT
for whichis_nothrow_move_constructible_v<T>
istrue
.Clearly, you did not implement this optimization.
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;
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>>
.
-
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\$Indiana Kernick– Indiana Kernick2019年04月27日 04:45:14 +00:00Commented 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.– L. F.2019年04月27日 04:47:53 +00:00Commented Apr 27, 2019 at 4:47
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.
Explore related questions
See similar questions with these tags.
std::any
, I'd say it's rarely useful, because if you know the possible type values you'd usestd::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 abstractObject
type. Nonetheless it can find a use in evolutive / pluggable / distributed programs, where different components can try and recognize what astd::any
really is. \$\endgroup\$any_cast
of a pointer, you check if the pointer is null beforedynamic_cast
, and then again after. Butdynamic_cast<T>(nullptr)
just gives back a null pointer of typeT
(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\$anything
and castinganything
. I’m checkinganything
and castinganything->storage
. If either of the null checks are removed then I’d be dereferencing a null pointer. \$\endgroup\$