Took a shot at implementing std::any
. I think this should cover a majority of the functionality of the standard version, except there may be some missing auxiliary functions, and the overload resolution may not be exactly as is written in the standard.
#include <memory>
#include <typeinfo>
#include <utility>
#include <stdexcept>
struct bad_any_cast: std::exception {
const char* what() const noexcept override {
return "bad_any_cast: failed type conversion using AnyCast";
}
};
class Any {
public:
constexpr Any() noexcept = default;
template<typename T, typename Decayed = std::decay_t<T>>
requires(!std::is_same_v<Decayed, Any>)
Any(T&& value) {
m_holder = std::make_unique<holder<Decayed>>(std::forward<T>(value));
}
template<typename T, typename... Args>
Any(std::in_place_type_t<T> type, Args&&...args) {
emplace<T>(std::forward<Args>(args)...);
}
Any(const Any& other) {
if (other.m_holder) {
m_holder = std::unique_ptr<base_holder>(other.m_holder->clone());
}
}
Any& operator=(const Any& other) {
if (this != &other) {
Any temp{other};
swap(temp);
}
return *this;
}
Any(Any&& other) noexcept : m_holder(std::move(other.m_holder)) {}
Any& operator=(Any&& other) noexcept {
if (this != &other) {
m_holder = std::move(other.m_holder);
}
return *this;
}
template<typename T, typename... Args>
void emplace(Args&&... args) {
m_holder = std::make_unique<holder<T>>(std::forward<Args>(args)...);
}
void swap(Any& other) noexcept {
std::swap(m_holder, other.m_holder);
}
void reset() noexcept {
m_holder.reset();
}
bool has_value() const noexcept {
return bool(*this);
}
explicit operator bool() const noexcept {
return m_holder != nullptr;
}
~Any() = default;
private:
template<typename U>
friend U AnyCast(const Any& value);
template<typename U>
friend U AnyCast(Any& value);
template<typename U>
friend U AnyCast(Any&& value);
template<typename U>
friend U* AnyCast(Any* value);
struct base_holder {
virtual const std::type_info &type() const noexcept = 0;
virtual base_holder* clone() const = 0;
virtual ~base_holder() = default;
};
template<typename T>
struct holder: base_holder {
template<typename U>
holder(U&& value):
item{std::forward<U>(value)} {}
template<typename... Args>
holder(Args&&... args):
item{T(std::forward<Args>(args)...)} {}
holder* clone() const override {
return new holder<T>{std::move(item)};
}
const std::type_info &type() const noexcept override {
return typeid(T);
}
T item;
};
std::unique_ptr<base_holder> m_holder;
};
template<typename T>
T AnyCast(const Any& value) {
if (value.m_holder->type() != typeid(T)) {
throw bad_any_cast();
}
return dynamic_cast<Any::holder<T>&>(*value.m_holder).item;
}
template<typename T>
T AnyCast(Any&& value) {
if (value.m_holder->type() != typeid(T)) {
throw bad_any_cast();
}
return dynamic_cast<Any::holder<T>&>(*value.m_holder).item;
}
template<typename T>
T AnyCast(Any& value) {
if (value.m_holder->type() != typeid(T)) {
throw bad_any_cast();
}
return dynamic_cast<Any::holder<T>&>(*value.m_holder).item;
}
template<typename T>
T* AnyCast(Any* value) {
if (!value || value->m_holder->type() != typeid(T)) {
return nullptr;
}
return &(dynamic_cast<Any::holder<T>&>(*value->m_holder).item);
}
template<typename T, typename...Args>
Any makeAny(Args&&... args) {
return Any(std::in_place_type<T>, std::forward<Args>(args)...);
}
The class is very popular with lots of friends, most of which do very similar things. Is there some way I can consolidate this? Why is any_cast
a separate function and not part of the class anyway?
Finally, some tests partially inspired by cppreference examples.
#ifdef BOOST_TEST_DYN_LINK
#include <boost/test/unit_test.hpp>
#else
#include <boost/test/included/unit_test.hpp>
#endif // BOOST_TEST_DYN_LINK
#include <boost/test/data/monomorphic.hpp>
#include <boost/test/data/test_case.hpp>
#include "Any.h"
struct A {
int age;
std::string name;
double salary;
A(int age, std::string name, double salary)
: age(age), name(std::move(name)), salary(salary) {}
};
BOOST_AUTO_TEST_CASE(default_constructor_test) {
Any a;
BOOST_CHECK(!a);
BOOST_CHECK(!a.has_value());
}
BOOST_AUTO_TEST_CASE(basic_int_test) {
Any a{7};
BOOST_CHECK_EQUAL(AnyCast<int>(a), 7);
BOOST_CHECK(a.has_value());
}
BOOST_AUTO_TEST_CASE(in_place_type_test) {
Any a(std::in_place_type<A>, 30, "Ada", 1000.25);
BOOST_CHECK_EQUAL(AnyCast<A>(a).age, 30);
BOOST_CHECK_EQUAL(AnyCast<A>(a).name, "Ada");
BOOST_CHECK_EQUAL(AnyCast<A>(a).salary, 1000.25);
}
BOOST_AUTO_TEST_CASE(bad_cast_test) {
Any a{7};
BOOST_CHECK_THROW(AnyCast<float>(a), bad_any_cast);
}
BOOST_AUTO_TEST_CASE(type_change_test) {
Any a{7};
BOOST_CHECK_EQUAL(AnyCast<int>(a), 7);
BOOST_CHECK_THROW(AnyCast<std::string>(a), bad_any_cast);
a = std::string("hi");
BOOST_CHECK_EQUAL(AnyCast<std::string>(a), "hi");
BOOST_CHECK_THROW(AnyCast<int>(a), bad_any_cast);
}
BOOST_AUTO_TEST_CASE(reset_test) {
Any a{7};
BOOST_CHECK_EQUAL(AnyCast<int>(a), 7);
a.reset();
BOOST_CHECK(!a.has_value());
}
BOOST_AUTO_TEST_CASE(pointer_test) {
Any a{7};
int *i = AnyCast<int>(&a);
BOOST_CHECK_EQUAL(*i, 7);
}
BOOST_AUTO_TEST_CASE(emplacement_test) {
Any a;
BOOST_CHECK(!a.has_value());
a.emplace<A>(30, "Ada", 1000.25);
BOOST_CHECK_EQUAL(AnyCast<A>(a).age, 30);
BOOST_CHECK_EQUAL(AnyCast<A>(a).name, "Ada");
BOOST_CHECK_EQUAL(AnyCast<A>(a).salary, 1000.25);
a.emplace<A>(50, "Bob", 500.5);
BOOST_CHECK_EQUAL(AnyCast<A>(a).age, 50);
BOOST_CHECK_EQUAL(AnyCast<A>(a).name, "Bob");
BOOST_CHECK_EQUAL(AnyCast<A>(a).salary, 500.5);
}
BOOST_AUTO_TEST_CASE(swap_test) {
Any a1{7};
Any a2{A{30, "Ada", 1000.25}};
a1.swap(a2);
BOOST_CHECK_EQUAL(AnyCast<int>(a2), 7);
BOOST_CHECK_EQUAL(AnyCast<A>(a1).age, 30);
BOOST_CHECK_EQUAL(AnyCast<A>(a1).name, "Ada");
BOOST_CHECK_CLOSE(AnyCast<A>(a1).salary, 1000.25, 0.0001); // Use BOOST_CHECK_CLOSE for floating point comparisons
}
BOOST_AUTO_TEST_CASE(make_any_test) {
Any a1 = makeAny<int>(7);
Any a2 = makeAny<A>(30, "Ada", 1000.25);
a1.swap(a2);
BOOST_CHECK_EQUAL(AnyCast<int>(a2), 7);
BOOST_CHECK_EQUAL(AnyCast<A>(a1).age, 30);
BOOST_CHECK_EQUAL(AnyCast<A>(a1).name, "Ada");
BOOST_CHECK_EQUAL(AnyCast<A>(a1).salary, 1000.25);
}
BOOST_AUTO_TEST_CASE(move_test) {
Any a1{42};
Any a2{std::move(a1)};
BOOST_CHECK(!a1.has_value());
BOOST_CHECK_EQUAL(AnyCast<int>(a2), 42);
}
BOOST_AUTO_TEST_CASE(copy_test) {
Any original{A{30, "Ada", 1000.25}};
Any copy{original};
A original_casted = AnyCast<A>(original);
A copy_casted = AnyCast<A>(copy);
original_casted.age = 40;
BOOST_CHECK_NE(original_casted.age, copy_casted.age);
}
BOOST_AUTO_TEST_CASE(self_assignment_test) {
Any a{5};
a = a; // self-assignment
BOOST_CHECK_EQUAL(AnyCast<int>(a), 5);
}
BOOST_AUTO_TEST_CASE(nested_any_test) {
Any inner{42};
Any outer;
outer.emplace<Any>(inner);
BOOST_CHECK_THROW(AnyCast<int>(outer), bad_any_cast);
BOOST_CHECK_EQUAL(AnyCast<Any>(outer).has_value(), true);
}
BOOST_AUTO_TEST_CASE(large_object_test) {
std::vector<int> largeVector(1000000, 5); // A vector with 1 million ints.
Any a{largeVector};
BOOST_CHECK_EQUAL(AnyCast<std::vector<int>>(a).size(), 1000000);
}
static int destructorCounter = 0;
struct TestDestruction {
~TestDestruction() {
destructorCounter++;
}
};
BOOST_AUTO_TEST_CASE(destructor_call_test) {
{
Any a;
a.emplace<TestDestruction>();
} // scope to ensure a is destroyed
BOOST_CHECK_EQUAL(destructorCounter, 1);
}
struct ExceptionThrower {
ExceptionThrower() {
throw std::runtime_error("Exception during construction");
}
};
BOOST_AUTO_TEST_CASE(exception_safety_test) {
BOOST_CHECK_THROW(Any a{ExceptionThrower{}}, std::runtime_error);
}
```
2 Answers 2
The copy constructor moves the value from other
There is a bug in holder::clone()
:
return new holder<T>{std::move(item)};
You shouldn't std::move(item)
if you want to clone it. Your tests all pass because you either used trivial types, or std::string
s with such a short string that small-string optimization kicks in and never actually causes an allocation that would have been moved.
Let clone()
return a std::unique_ptr
Use std::unique_ptr
s as early as possible, this simplifies the code and leaves less chance for manual memory management errors.
About those friends
The class is very popular with lots of friends, most of which do very similar things. Is there some way I can consolidate this? Why is
any_cast
a separate function and not part of the class anyway?
Probably because static_cast<>()
and friends already exist, which could not have been made class members, and any_cast<>()
just follows that pattern. This is actually nice; it means it's easier to learn with less surprises.
It's not a lot of friends anyway, std::any
only has three: std::swap<std::any>()
, std::any_cast<>()
and std::make_any<>()
. It's just that each of these can have multiple overloads.
It also allows the convenience of casting a pointer to a std::any
to a pointer to its value, something which would not be possible with a member function (unless you give that one a different name).
In-place construction slightly wrong?
The constructor of holder
that takes a variadic number of parameters uses the following to initialize item
:
item{T(std::forward<Args>(args)...)}
I would expect:
item(std::forward<Args>(args)...)
Note that a type might have neither move nor copy constructors, or copying/moving might have an overhead you should avoid.
Unnecessary code
There are a few lines of code that could be removed:
- Instead of defaulting it, you can just omit the destructor of
Any
(but keep the one inbase_holder
, as that one needs to bevirtual
). - You use the copy-and-swap idiom in the copy constructor, so you don't need to check for
this != &other
. - The move constructor and move assignment operators can be
default
ed.
-
\$\begingroup\$ Thanks for the help! Just had one question, I understand why
std::move()
-ing the item inclone()
is incorrect (no clue why I did that) but I'm not actually able to write a failing test case for it. I tried the following but the test still passes, although I suppose maybe the implementation leaves a moved-fromstd::string
still containing the value. Is the answer to create a custom type where this will not be the case? \$\endgroup\$jdav22– jdav222023年09月14日 18:40:32 +00:00Commented Sep 14, 2023 at 18:40 -
\$\begingroup\$ Attempt at failing test: BOOST_AUTO_TEST_CASE(big_string_test) { std::string s = "ljadfjkldkldkldak;slfldafklsdkjlsdafsdaf;jlk;jklddfsa"; Any a{s}; Any b{a}; BOOST_CHECK_EQUAL(AnyCast<std::string>(a), s); BOOST_CHECK_EQUAL(AnyCast<std::string>(b), s); } \$\endgroup\$jdav22– jdav222023年09月14日 18:40:55 +00:00Commented Sep 14, 2023 at 18:40
-
1\$\begingroup\$ The state of an object after a move is such that it can still be safely destructed, but no other guarantee is made. A class can decide to leave the original contents, clear the contents, or anything else. Accessing it afterwards might thus not be safe, but there is no guarantee it will crash. Creating your own custom type would help as you can then control exactly what it does during a move. Also consider using Valgrind or AddressSanitizer when running your tests. \$\endgroup\$G. Sliepen– G. Sliepen2023年09月14日 20:52:46 +00:00Commented Sep 14, 2023 at 20:52
-
1\$\begingroup\$ Incidentally, one of my go-to test cases for in-place construction type methods is:
Any x(std::in_place_type<std::string>, 5, 'a');
That way, if you accidentally tried using brace-initialization in the implementation, you would catch the bug early that it would interpret this case as wanting theinitializer_list
overload instead of the count and fill character overload. \$\endgroup\$Daniel Schepler– Daniel Schepler2023年09月15日 15:24:36 +00:00Commented Sep 15, 2023 at 15:24
In addition to G. Sliepen’s excellent answer, and since you asked specifically about how you could simplify the friend
functions:
You Can Turn the any_cast
Overloads into Wrappers
You could simplify the definitions of the any_cast
overloads by making them short wrappers. First (thanks to Daniel Schepler for pointing out a serious mistake), the any_cast<Any*>
function has different behavior than the others on failure, returning nullptr
. It’s therefore easier to implement the versions that throw bad_any_cast;
in terms of this than the other way around: In fact, the Any&
overload is specified to return the dereferenced valur of the Any*
(with certain qualifiers removed and a static_cast
) and the Any&&
overload is specified to return std::move
of that.
This has the advantage that you don’t repeat yourself. You write the check for validity once, in the Any*
version, and you write the code to check it and possibly throw bad_any_cast
once, in the Any&
version. The other implementations that need them, wrap the version that provides them, with no run-time overhead.
-
\$\begingroup\$ The pointer cast has a different behavior on failure, in that it returns
nullptr
intsead of throwing a bad cast exception. So probably, you would want to make the pointer version the base version, and then the reference version could be wrappers which check the return value and throw an exception if it'snullptr
. \$\endgroup\$Daniel Schepler– Daniel Schepler2023年09月15日 15:27:08 +00:00Commented Sep 15, 2023 at 15:27 -
\$\begingroup\$ @DanielSchepler Oh, good catch! I’m in a hurry right now, but I really need to correct that error. \$\endgroup\$Davislor– Davislor2023年09月15日 16:03:55 +00:00Commented Sep 15, 2023 at 16:03