3
\$\begingroup\$

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);
}
```
\$\endgroup\$

2 Answers 2

5
\$\begingroup\$

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::strings 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_ptrs 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 in base_holder, as that one needs to be virtual).
  • 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 defaulted.
\$\endgroup\$
4
  • \$\begingroup\$ Thanks for the help! Just had one question, I understand why std::move()-ing the item in clone() 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-from std::string still containing the value. Is the answer to create a custom type where this will not be the case? \$\endgroup\$
    – jdav22
    Commented 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
    Commented 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
    Commented 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 the initializer_list overload instead of the count and fill character overload. \$\endgroup\$ Commented Sep 15, 2023 at 15:24
4
\$\begingroup\$

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.

\$\endgroup\$
2
  • \$\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's nullptr. \$\endgroup\$ Commented 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
    Commented Sep 15, 2023 at 16:03

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.