开发者

On what platforms will this crash, and how can I improve it?

I've written the rudiments of a class for creating dynamic structures in C++. Dynamic structure members are stored contiguously with (as far as my tests indicate) the same padding that the compiler would insert in the equivalent static structure. Dynamic structures can thus be implicitly converted to static structures for interoperability with existing APIs.

Foremost, I don't trust myself to be able to write Boost-quality code that can compile and work on more or less any platform. What parts of this code are dangerously in need of modification?

I have one other design-related question: Is a templated get accessor the only way of providing the compiler with the requisite static type information for type-safe code? As it is, the user of dynamic_struct must specify the type of the member they are accessing, whenever they access it. If that type should change, all of the accesses become invalid, and will either cause spectacular crashes—or worse, fail silently. And it can't be caught at compile time. That's a huge risk, and one I'd like to remedy.

Example of usage:

struct Test {

    char a, b, c;
    int i;
    Foo object;

};

void bar(const Test&);

int main(int argc, char** argv) {

    dynamic_struct<std::string> ds(sizeof(Test));

    ds.append<char>("a") = 'A';
    ds.append<char>("b") = '2';
    ds.append<char>("c") = 'D';
    ds.append<int>("i") = 123;
    ds.append<Foo>("object");
    bar(ds);

}

And the code follows:

//
// dynamic_struct.h
//
// Much omitted for brevity.
//


/**
 * For any type, determines the alignment imposed by the compiler.
 */
template<class T>
class alignment_of {
private:

    struct alignment {

        char a;
        T b;

    }; // struct alignment

public:

    enum { value = sizeof(alignment) - sizeof(T) };

}; // class alignment_of


/**
 * A dynamically-created structure, whose fields are indexed by keys of
 * some type K, which can be substituted at runtime for any structure
 * with identical members and packing.
 */
template<class K>
class dynamic_struct {
public:


    // Default maximum structure size.
    static const int DEFAULT_SIZE = 32;


    /**
     * Create a structure with normal inter-element padding.
     */
    dynamic_struct(int size = DEFAULT_SIZE) : max(size) {

        开发者_开发知识库data.reserve(max);

    } // dynamic_struct()


    /**
     * Copy structure from another structure with the same key type.
     */
    dynamic_struct(const dynamic_struct& structure) :
        members(structure.members), max(structure.max) {

        data.reserve(max);

        for (iterator i = members.begin(); i != members.end(); ++i)
            i->second.copy(&data[0] + i->second.offset,
                &structure.data[0] + i->second.offset);

    } // dynamic_struct()


    /**
     * Destroy all members of the structure.
     */
    ~dynamic_struct() {

        for (iterator i = members.begin(); i != members.end(); ++i)
            i->second.destroy(&data[0] + i->second.offset);

    } // ~dynamic_struct()


    /**
     * Get a value from the structure by its key.
     */
    template<class T>
    T& get(const K& key) {

        iterator i = members.find(key);

        if (i == members.end()) {

            std::ostringstream message;
            message << "Read of nonexistent member \"" << key << "\".";
            throw dynamic_struct_access_error(message.str());

        } // if

        return *reinterpret_cast<T*>(&data[0] + i->second.offset.offset);

    } // get()


    /**
     * Append a member to the structure.
     */
    template<class T>
    T& append(const K& key, int alignment = alignment_of<T>::value) {

        iterator i = members.find(key);

        if (i != members.end()) {

            std::ostringstream message;
            message << "Add of already existing member \"" << key << "\".";
            throw dynamic_struct_access_error(message.str());

        } // if

        const int modulus = data.size() % alignment;
        const int delta = modulus == 0 ? 0 : sizeof(T) - modulus;

        if (data.size() + delta + sizeof(T) > max) {

            std::ostringstream message;

            message << "Attempt to add " << delta + sizeof(T)
                << " bytes to struct, exceeding maximum size of "
                << max << ".";

            throw dynamic_struct_size_error(message.str());

        } // if

        data.resize(data.size() + delta + sizeof(T));

        new (static_cast<void*>(&data[0] + data.size() - sizeof(T))) T;

        std::pair<iterator, bool> j = members.insert
            ({key, member(data.size() - sizeof(T), destroy<T>, copy<T>)});

        if (j.second) {

            return *reinterpret_cast<T*>(&data[0] + j.first->second.offset);

        } else {

            std::ostringstream message;
            message << "Unable to add member \"" << key << "\".";
            throw dynamic_struct_access_error(message.str());

        } // if

    } // append()


    /**
     * Implicit checked conversion operator.
     */
    template<class T>
    operator T&() { return as<T>(); }


    /**
     * Convert from structure to real structure.
     */
    template<class T>
    T& as() {

        // This naturally fails more frequently if changed to "!=".
        if (sizeof(T) < data.size()) {

            std::ostringstream message;

            message << "Attempt to cast dynamic struct of size "
                << data.size() << " to type of size " << sizeof(T) << ".";

            throw dynamic_struct_size_error(message.str());

        } // if

        return *reinterpret_cast<T*>(&data[0]);

    } // as()


private:


    // Map from keys to member offsets.
    map_type members;

    // Data buffer.
    std::vector<unsigned char> data;

    // Maximum allowed size.
    const unsigned int max;


}; // class dynamic_struct


There's nothing inherently wrong with this kind of code. Delaying type-checking until runtime is perfectly valid, although you will have to work hard to defeat the compile-time type system. I wrote a homogenous stack class, where you could insert any type, which functioned in a similar fashion.

However, you have to ask yourself- what are you actually going to be using this for? I wrote a homogenous stack to replace the C++ stack for an interpreted language, which is a pretty tall order for any particular class. If you're not doing something drastic, this probably isn't the right thing to do.

In short, you can do it, and it's not illegal or bad or undefined and you can make it work - but you only should if you have a very desperate need to do things outside the normal language scope. Also, your code will die horrendously when C++0x becomes Standard and now you need to move and all the rest of it.

The easiest way to think of your code is actually a managed heap of a miniature size. You place on various types of object.. they're stored contiguously, etc.

Edit: Wait, you didn't manage to enforce type safety at runtime either? You just blew compile-time type safety but didn't replace it? Let me post some far superior code (that is somewhat slower, probably).

Edit: Oh wait. You want to convert your dynamic_struct, as the whole thing, to arbitrary unknown other structs, at runtime? Oh. Oh, man. Oh, seriously. What. Just no. Just don't. Really, really, don't. That's so wrong, it's unbelievable. If you had reflection, you could make this work, but C++ doesn't offer that. You can enforce type safety at runtime per each individual member using dynamic_cast and type erasure with inheritance. Not for the whole struct, because given a type T you can't tell what the types or binary layout is.


I think the type-checking could be improved. Right now it will reinterpret_cast itself to any type with the same size.

Maybe create an interface to register client structures at program startup, so they may be verified member-by-member — or even rearranged on the fly, or constructed more intelligently in the first place.

#define REGISTER_DYNAMIC_STRUCT_CLIENT( STRUCT, MEMBER ) \
    do dynamic_struct::registry< STRUCT >() // one registry obj per client type \
        .add( # MEMBER, &STRUCT::MEMBER, offsetof( STRUCT, MEMBER ) ) while(0)
        //    ^ name as str ^ ptr to memb ^ check against dynamic offset


I have one question: what do you get out of it ?

I mean it's a clever piece of code but:

  • you're fiddling with memory, the chances of blow-up are huge
  • it's quite complicated too, I didn't get everything and I would certainly have to pose longer...

What I am really wondering is what you actually want...

For example, using Boost.Fusion

struct a_key { typedef char type; };
struct object_key { typedef Foo type; };

typedef boost::fusion<
  std::pair<a_key, a_key::type>,
  std::pair<object_key, object_key::type>
> data_type;

int main(int argc, char* argv[])
{
  data_type data;
  boost::fusion::at_key<a_key>(data) = 'a'; // compile time checked
}

Using Boost.Fusion you get compile-time reflection as well as correct packing.

I don't really see the need for "runtime" selection here (using a value as key instead of a type) when you need to pass the right type to the assignment anyway (char vs Foo).

Finally, note that this can be automated, thanks to preprocessor programming:

DECLARE_ATTRIBUTES(
  mData,
  (char, a)
  (char, b)
  (char, c)
  (int, i)
  (Foo, object)
)

Not much wordy than a typical declaration, though a, b, etc... will be inner types rather than attributes names.

This has several advantages over your solution:

  • compile-time checking
  • perfect compliance with default generated constructors / copy constructors / etc...
  • much more compact representation
  • no runtime lookup of the "right" member
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜