How to store variant data in C++
Asked Answered
R

5

24

I'm in the process of creating a class that stores metadata about a particular data source. The metadata is structured in a tree, very similar to how XML is structured. The metadata values can be integer, decimal, or string values.

I'm curious if there is a good way in C++ to store variant data for a situation like this. I'd like for the variant to use standard libraries, so I'm avoiding the COM, Ole, and SQL VARIANT types that are available.

My current solution looks something like this:

enum MetaValueType
{
    MetaChar,
    MetaString,
    MetaShort,
    MetaInt,
    MetaFloat,
    MetaDouble
};

union MetaUnion
{
    char cValue;
    short sValue;
    int iValue;
    float fValue;
    double dValue;
};

class MetaValue
{
...
private:
    MetaValueType ValueType;
    std::string StringValue;
    MetaUnion VariantValue;
};

The MetaValue class has various Get functions for obtaining the currently stored variant value, but it ends up making every query for a value a big block of if/else if statements to figure out which value I'm looking for.

I've also explored storing the value as only a string, and performing conversions to get different variant types out, but as far as I've seen this leads to a bunch of internal string parsing and error handling which isn't pretty, opens up a big old can of precision and data loss issues with floating point values, and still doesn't eliminate the query if/else if issue stated above.

Has anybody implemented or seen something that's cleaner to use for a C++ variant data type using standard libraries?

Raggletaggle answered 16/10, 2008 at 15:9 Comment(2)
Might I suggest phrasing your title as a descriptive question? Perhaps something like "How to store variant data in C++?" I think you'll get more responses!Copacetic
Hmm … I understood the title at once.Slovenly
S
33

As of C++17, there’s std::variant.

If you can’t use that yet, you might want Boost.Variant. A similar, but distinct, type for modelling polymorphism is provided by std::any (and, pre-C++17, Boost.Any).

Just as an additional pointer, you can look for “type erasure”.

Slovenly answered 16/10, 2008 at 15:12 Comment(1)
Herb Sutter suggests this answer as well: herbsutter.com/2008/06/20/…Snick
A
13

While Konrad's answer (using an existing standardized solution) is certainly preferable to writing your own bug-prone version, the boost variant has some overheads, especially in copy construction and memory.

A common customized approach is the following modified Factory Pattern:

  1. Create a Base interface for a generic object that also encapsulates the object type (either as an enum), or using 'typeid' (preferable).
  2. Now implement the interface using a template Derived class.
  3. Create a factory class with a templateized create function with signature:

template <typename _T> Base * Factory::create ();

This internally creates a Derived<_T> object on the heap, and retuns a dynamic cast pointer. Specialize this for each class you want implemented.

Finally, define a Variant wrapper that contains this Base * pointer and defines template get and set functions. Utility functions like getType(), isEmpty(), assignment and equality operators, etc can be appropriately implemented here.

Depending on the utility functions and the factory implementation, supported classes will need to support some basic functions like assignment or copy construction.

Arrange answered 16/11, 2009 at 3:29 Comment(3)
Again, if you can, please use the boost classes, unless microsecond-level timing is important to you.Arrange
FIFY: nanosecond-level.Amortizement
:) nanosecond per API use - in my experience, a fundamental implementation like this adds up.Arrange
A
9

You can also go down to a more C-ish solution, which would have a void* the size of a double on your system, plus an enum for which type you're using. It's reasonably clean, but definitely a solution for someone who feels wholly comfortable with the raw bytes of the system.

Archaeology answered 16/10, 2008 at 15:30 Comment(8)
void * is pretty evil. Prefer more elegant solutions when possible.Celio
It's a very elegant solution...if you are comfortable working with raw memory.Archaeology
I don't agree that void pointers are elegant. You are bypassing the type system, its not a matter of being comfortable working with raw memory, its a matter of getting all the help from the compiler that you possible can.Cabob
It's OK to bypass the type system if you know what you're doing. C++/C are not 100% typed anyway.Archaeology
For C ++, void* pointers are pure evil. With all the disadvantages of raw pointers, you think you can't get worse. It turns out you can... 1. it bypasses type-checking. Even with enums (e.g. if (myType == MY_CLASS) { MYCLASS* val = (MYCLASS*)(voidPtr); }, a polymorphic MYCLASS is gonna wreak havoc with memory. Using a base class pointer and dynamic casts is marginally better. 2. possibility of splice-deleting (and no way to check). 3. the 'double' solution suggested suffers from endian-dependency.Arrange
@Arrange When you want to have typesafety then you have to use java than c or c++. Most times there are better options than using void * pointers. But it defently make sense to take a look at all options. Using void * pointers would also boost your skills. Because you make a lot of mistakes and lern to solve them. :)Wallen
that's like saying Uganda's laws are inconsistent and have loopholes, so it's lawless and I'm justified commiting murders there. Granted C/C++'s type-system is not fool-proof, and using pointers, C-style typecasts and reinterpret_cast can allow you to get around. That doesn't mean you toss every rule/recommendation out of the window. (No offense to Ugandans - it's an example)Arrange
To quiet the C++ void * nazis, just use char * instead of void *. They are the same size (check this).Terryn
D
7

C++17 now has std::variant which is exactly what you're looking for.

std::variant

The class template std::variant represents a type-safe union. An instance of std::variant at any given time either holds a value of one of its alternative types, or in the case of error - no value (this state is hard to achieve, see valueless_by_exception).

As with unions, if a variant holds a value of some object type T, the object representation of T is allocated directly within the object representation of the variant itself. Variant is not allowed to allocate additional (dynamic) memory.

Durst answered 13/2, 2017 at 18:8 Comment(0)
F
4

Although the question had been answered for a long time, for the record I would like to mention that QVariant in the Qt libraries also does this.

Because C++ forbids unions from including types that have non-default constructors or destructors, most interesting Qt classes cannot be used in unions. Without QVariant, this would be a problem for QObject::property() and for database work, etc.

A QVariant object holds a single value of a single type() at a time. (Some type()s are multi-valued, for example a string list.) You can find out what type, T, the variant holds, convert it to a different type using convert(), get its value using one of the toT() functions (e.g., toSize()) and check whether the type can be converted to a particular type using canConvert().

Feaze answered 15/4, 2015 at 6:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.