Construct boost::type_erasure::any from C string but store as std::string

183 Views Asked by At

Is it possible to declare a boost::type_erasure::any in a way that constructing and assigning from string literal or char const* automatically copies the string into a std::string and stores that in the boost::type_erasure::any object?

By default, a boost::type_erasure::any just stores the string pointer.

The aim is to avoid a source of errors when users of my any type assign a string pointer to it, assuming a copy will be made (like std::string does) and then the lifetime of the string ends before my any is read, causing a crash.

Example:

#include <boost/type_erasure/operators.hpp>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/any_cast.hpp>
#include <boost/type_erasure/relaxed.hpp>
#include <boost/mpl/vector.hpp>

#include <iostream>

namespace te = boost::type_erasure;

using my_any = te::any< boost::mpl::vector<
    te::copy_constructible<>,
    te::destructible<>,
    te::typeid_<>,
    te::relaxed
    /* I believe some changes here would do the trick */
    >>;

using namespace std;

int main()
{
    // Store an std::string by explicitly calling string constructor.
    my_any a = string("abc");

    // The following should copy-construct an std::string too but it just stores
    // the string pointer.
    my_any b = "abc";

    // Works as expected.
    cout << te::any_cast<string>( a ) << endl;

    // This crashes because the underlying type of b is not std::string.
    // With some changes to the my_any type this shouldn't crash anymore.
    cout << te::any_cast<string>( b ) << endl;
}

Live Demo.

2

There are 2 best solutions below

5
Yakk - Adam Nevraumont On

No, any stores anything. const char* is anything.

Note that "hello"s is a literal of type std::string.

2
zett42 On

I'm posting an answer to my own question in the hope to clarify the intended use of the boost::type_erasure::any without making the original question too verbose.

This answer shows a possible workaround by hiding the boost::type_erasure::any behind the interface of another class and providing an overload of a setter method for all types convertible to std::string. This overload takes care about converting char pointers and char arrays to std::string.

I think one additional overload is not that bad, but ideally I would like to avoid that boilerplate and make the any type aware of how to convert C strings. That brings us back to my original question.

#include <iostream>
#include <unordered_map>
#include <string>
#include <type_traits>

#include <boost/type_erasure/operators.hpp>
#include <boost/type_erasure/any.hpp>
#include <boost/type_erasure/any_cast.hpp>
#include <boost/type_erasure/relaxed.hpp>
#include <boost/mpl/vector.hpp>

namespace te = boost::type_erasure;

// A class to store attributes of any type by name.
class Attributes
{
public:
    using Key = std::string;

    // A type that can store any value (similar to std::any).
    using AnyValue = te::any< boost::mpl::vector<
        te::copy_constructible<>,
        te::destructible<>,
        te::typeid_<>,
        te::relaxed
        >>;

    // Overload for all types that ain't strings.
    template< typename T >
        std::enable_if_t< !std::is_convertible<T, std::string>::value, 
    void > SetAttr( Key const& name, T&& value )
    {
        m_attr.insert_or_assign( name, std::forward<T>( value ) );
    }

    // Convert to std::string for all convertible types
    // (char pointer and char array included).
    template< typename T >
        std::enable_if_t< std::is_convertible<T, std::string>::value, 
    void > SetAttr( Key const& name, T&& value )
    {
        m_attr.insert_or_assign( name, std::string( std::forward<T>( value ) ) );
    }

    template< typename T >
    T GetAttr( Key const& name ) const 
    { 
        return te::any_cast<T>( m_attr.at( name ) ); 
    }

private:
    std::unordered_map<Key, AnyValue> m_attr;
};

The following example shows how different kind of strings can be passed to Attributes::SetAttr() and be queried generically via Attributes::GetAttr<std::string>():

using namespace std;

Attributes a;
// Works even w/o special care.
a.SetAttr( "key1", string("foo") );

// Without the SetAttr() overload for strings, user would have to remind 
// to cast back to char const* when calling MyClass::GetAttr().
a.SetAttr( "key2", "bar" );

// Without the SetAttr() overload for strings, a later call to GetAttr()
// would cause a crash because we are passing pointers to temporary objects.
{
    // test arrays
    char temp1[] = { 'b', 'a', 'z', 0 };
    a.SetAttr( "key3", temp1 );
    char const temp2[] = { 'b', 'i', 'm', 0 };
    a.SetAttr( "key4", temp2 );

    // test pointers
    a.SetAttr( "key5", &temp1[0] );
    a.SetAttr( "key6", &temp2[0] );
}

try
{
    // When getting a string attribute we no longer have to care about how it was
    // passed to SetAttr(), we can simply cast to std::string in all cases.
    cout << "'" << a.GetAttr<string>( "key1" ) << "'" << endl;
    cout << "'" << a.GetAttr<string>( "key2" ) << "'" << endl;
    cout << "'" << a.GetAttr<string>( "key3" ) << "'" << endl;
    cout << "'" << a.GetAttr<string>( "key4" ) << "'" << endl;
    cout << "'" << a.GetAttr<string>( "key5" ) << "'" << endl;
    cout << "'" << a.GetAttr<string>( "key6" ) << "'" << endl;
}
// boost::type_erasure::bad_any_cast or std::out_of_range
catch( std::exception& e )
{
    cout << "Error: " << e.what() << endl;
}

Live Demo.