Jump to content
  • Advertisement
Sign in to follow this  
irreversible

A short discussion on PODs

This topic is 1501 days old which is more than the 365 day threshold we allow for new replies. Please post a new topic.

If you intended to correct an error in the post then please contact us.

Recommended Posts

Warning: potentially boring rambling ahead.

 

PODs, by definition, used to be simple: trivial structures that contain only data. That's it.

 

Times have changed, however, and the following is a short summary of what's going on in my head regarding the matter. Mind you that this is not (nor do I wish it to be) a discussion on the validity of my decision to roll my own container code vs using the Standard Template Library, but rather an issue of design with respect to data representation vs functionality vs compliance with the C++ standard.

 

With the relaxation of rules in C++11, the distinction regarding PODs has become more complex and this has gotten a small part in the back of my brain looking for options to enforce robustness in my code. Fundamentally, my question stems form the fact that I have a couple of custom containers (in particular a POD-like replacement for std::vector and a custom hashmap implementation, which internally uses my own vector container), which make extensive use of my own memory manager and as far as I can tell provide some additional functionality over the STL that I just prefer to have (for instance my VECTOR container exposes an acquire() method, which emits a pointer to the next unused object if reserve() has been used to preallocate the memory). I have trivial support for move operators and I'm wrapping simpler (or rather more crude) memory management than what std::vector provides. I do recognize, however, that ultimately it's no replacement for std::vector, focusing more on the containment of fundamentally simple structures. Like PODs. In fact, my VECTOR's support for rvalue insertion via move assignment in a way works against the logic of how the memory is managed internally.

 

That being said, there are cases when a logical distinction needs to be made and this directly ties to the definition of a POD - even if the distinction only exists on paper. The discussion is thereby largely academic, but it's bothering me enough in terms of code organization that I'd like some thoughts on it.

 

Most notably, a trivial POD cannot have a constructor or a destructor, or for that matter, recursively even members that do. Not to mention a copy constructor/assignment operator.

 

The structures my VECTOR container is designed for, however - for the most part - do take advantage of custom initialization and in some cases destruction of member objects - pretty much like STL containers. At the same time the objects are assumed to not have a virtual base class/member functions and must remain directly copiable. A simple example would be the IEntity structure, which among simple values contains things like smart pointers to its type and name (which in turn have constructors/destructors), a hashmap and several vectors of child entities, which in turn wrap smart pointers to dynamic data. This distinction makes many of the objects technically non-trivial, but they still retain the qualities inherent to simple PODs: I can use memcpy() to duplicate and memmove() to move them without risking breaking anything (other than perhaps some external references - but that's another topic). Nevertheless, most of the structures require some form of initialization to default values and in several cases destruction of dynamically allocated members (although I do handle allocation/deallocation of members externally in all cases and the VECTOR container functions more like what the word actually means: as a container).

 

Right now I'm explicitly calling the constructor when an object is being instantiated (whether that is by the compiler during allocation on the stack, via placement new by my memory manager on the heap, or by explicitly invoking placement new before returning a pointer to the object from VECTOR::acquire() ). Move semantics can largely be circumvented since the copies/moved instances never need instantiation and separate allocation, but are rather direct duplicates of the copy/move argument via CopyMemory(). It may not be the way STL does it, but it's pretty much as fast as it gets and I get the added bonus of full control over memory, plus my own functionality. In short, it works insofar as the actual code is involved.

 

Nevertheless, there are questions regarding enforcing POD-ness and, more generally, the strict definition of a POD as it stands in the standard.

 

Based on the above link, in order to conform to the standard, I either:

 

a) need to remove the constructor/destructor and provide external initialization (also to default/empty values)/deinitialization procedures to make the objects trivial. This would be inconvenient, but more importantly I'm not sure how to enforce it or in fact do it without cumbersome use of function pointers/template specialization.

b) have to acknowledge that the container fully supports standard layout PODs (which can have a constructor/destructor and user-defined copy/move constructors/operators), and accept that on paper this conflicts with the "relaxed rule" that all members in a standard layout POD have the same access control (eg all members be either private, protected or public)

 

The last statement (b) is what I've been getting at: while I imagine the following would work to enforce a trivial POD type (code not checked)...

struct TrivialPOD {
  int a;
  ISharedPtr<char> str;

  //disable default constructor and force "no user-defined constructor" in favor of external initialization
  TrvialPOD() = delete;
};

//rely on automatically generated move semantics; alternatively allocate on heap and return a pointer
TrivialPOD&& CreateTrivialPOD()
{
   TrivialPOD t;
   //assume default-constructed object needs some members to be non-NULL, so can't just ZeroMemory() it
   t.a = 1;
   str = null;

   return t;
}

void FreeTrivialPOD(TrivialPOD& pod)
{
   Delete(pod.str);
}

//A further complication is access control: in order to properly initialize
//private/protected members, the POD would need to have a friend class, which functions
//as a proxy. While this, in turn, is inconvenient, since it either requires static
//initializer procedures or the use of private function pointers when a container
//needs to initialize/deinitialize it. But, again, that's a question of semantics.

... I cannot really understand why standard POD types need to only have one type of access control (even if the requirement is relaxed). I routinely prefer some members to be private and some public for clarity reasons, and a user-defined constructor is a perfect way to handle this. Is there some underlying reason for this or is it just a matter of form? Or is the relaxation so loose that in effect the requirement doesn't exist?

 

EDIT: a third option to handle private/protected members would be to provide a non-constructor type default initializer member function, eg 'void [TrivialPOD]:InitializeDefault()' in each and every object that can be stored in the container. Would be silly, but at least it would be valid.

 

At the end of the day, my current "hybrid" approach works just fine: I use my containers for objects that have a definite size and can be trivially copied, but also provide at least a constructor to set default values (in all cases I delegate actual memory management to a separate module). And this is also where the (well, seemingly) academic question regarding the nature of a POD arises: it works as is and as far as I can tell I'm unlikely to miss something that might seriously break my code on a different platform. But at the same time, none of this is strictly compliant with the standard either. Moreover, while I'm not so OCD about compliance with the standard, there's the nagging question of "why the distinction between the two POD types in the first place?".

 

Sorry if this has been boring - I did warn you smile.png

Edited by irreversible

Share this post


Link to post
Share on other sites
Advertisement

POD in c++11 is two concepts: standard layout(which guarantees the order of elements), and trivial(no special actions are done while initializing, destructing and copying). Both combined guarantees same behavior with C, the plain old ways.

 

Identical access control is required for standard layout, since C++ require members with the same access control to be ordered in declaration order, but no requirement regarding different access control. The compiler is allowed to reorder different access control (maybe for optimization or hardware level read/write restrictions, I don't know why one would want to do that but it's their freedom), so having a class with multiple access control could break the layout, thus is non-standard layout.

 

Trivial-ness of a class is to ensure the compiler can use a no-brainer memcpy on your class and nothing will break. If you need a non-default constructor, that means you are doing something in it and memcpy-ing it could break it.

 

And ctor() = delete means no default constructor could be used. you need ctor() = default instead.

 

It's kind of late here so I didn't read all of your wall of text. I wonder why you want your class to be POD? You can still memcpy a non-POD if you know what you are doing, so having a constructor that only set the default values won't harm, and I bet the compiler can still optimize it while using default copy.
 

POD is only to guarantee to compiler nothing could possibly go wrong while doing certain operations. You can still ensure it yourself, even in a standard conformant way.

Share this post


Link to post
Share on other sites
POD is probably more than you want.

A type can be trivially copyable for instance but not trivially destructible. If you're trying to write the highest-performance vector without relying on a smart optimizing compiler that is general for all different types you're likely to throw at it, you're going to need to use helpers for copying, moving, constructing, and destructing that each individually check the appropriate type trait.

The STL includes helpers for most of these that will internally convert to a memcpy if the operation requested is trivial on the target type.

Share this post


Link to post
Share on other sites

The STL includes helpers for most of these that will internally convert to a memcpy if the operation requested is trivial on the target type.

 

Which functions are you referring to?

Share this post


Link to post
Share on other sites
std::copy, std::unitialized_copy, std::unitialized_fill, etc.

The ones for move aren't in the standard yet apparently, but I do believe they're planned additions for C++17. There's not one for destruct.

Writing them yourself is easy enough if you need them, though, assuming you don't want the full genericness that an STL implementation has to worry about (different source and destination iterator types, etc.). For instance, here's a trivial move for noexcept types usable for a vector implementation:

// begin and end is a range of values to move
// out points to unitialized memory that will contain the moved objects after this completes
// this will perform a copy instead of a move if move is not nothrow, as otherwise you'll
// end up with a half-moved range if an exception is thrown with no ability to recover sanely
template <typename T>
std::enable_if_t<!std::is_trivially_move_constructible_v<T>>
uninitialized_move(T* begin, T* end, T* out)
{
  while (begin != end) {
    new (out) T(std::move_if_noexcept(*begin));
    ++begin;
    ++out;
  }
}

template <typename T>
std::enable_if_t<std::is_trivially_move_constructible_v<T>>
uninitialized_move(T* begin, T* end, T* out)
{
  std::memcpy(out, begin, (end - begin) * sizeof(T));
}
You could simplify that a bit more with tag overloading.

Share this post


Link to post
Share on other sites

std::copy, std::unitialized_copy, std::unitialized_fill, etc.

The ones for move aren't in the standard yet apparently, but I do believe they're planned additions for C++17. There's not one for destruct.

Writing them yourself is easy enough if you need them, though, assuming you don't want the full genericness that an STL implementation has to worry about (different source and destination iterator types, etc.). For instance, here's a trivial move for noexcept types usable for a vector implementation:
 

// begin and end is a range of values to move
// out points to unitialized memory that will contain the moved objects after this completes
// this will perform a copy instead of a move if move is not nothrow, as otherwise you'll
// end up with a half-moved range if an exception is thrown with no ability to recover sanely
template <typename T>
std::enable_if_t<!std::is_trivially_move_constructible_v<T>>
uninitialized_move(T* begin, T* end, T* out)
{
  while (begin != end) {
    new (out) T(std::move_if_noexcept(*begin));
    ++begin;
    ++out;
  }
}

template <typename T>
std::enable_if_t<std::is_trivially_move_constructible_v<T>>
uninitialized_move(T* begin, T* end, T* out)
{
  std::memcpy(out, begin, (end - begin) * sizeof(T));
}
You could simplify that a bit more with tag overloading.

 

 

Ahh, I was aware of those, thought there was some I was missing ;)  One thing you need to consider is exception guarantee's.  If a throw occurs mid copy, you need to destruct the unfinished copies before re-throwing.

Share this post


Link to post
Share on other sites

One thing you need to consider is exception guarantee's.  If a throw occurs mid copy, you need to destruct the unfinished copies before re-throwing.


Yeah. Good catch.

This is an example of why I think that exceptions are the worst part of C++ (and to a lesser extent, every other language that has them) and why you should almost always just turn them off and use a simpler error handling approach.

The extra code bloat, mental overhead, runtime overhead, inevitable bugs, and inability to write certain containers/algorithms safely with exceptions just isn't worth supporting a rather mediocre error handling facility. Fatal error reporting (asserts), monadic errors, etc. solve more problems while introducing fewer new ones.

At the very least, enforce noexcept on your move operations. Or even do so on copy; you shouldn't be copying containers or anything that needs to allocate on copy (I'd even argue strongly that containers shouldn't even have copy operations but rather only fill-from operations).

Share this post


Link to post
Share on other sites

 

One thing you need to consider is exception guarantee's.  If a throw occurs mid copy, you need to destruct the unfinished copies before re-throwing.


Yeah. Good catch.

This is an example of why I think that exceptions are the worst part of C++ (and to a lesser extent, every other language that has them) and why you should almost always just turn them off and use a simpler error handling approach.

The extra code bloat, mental overhead, runtime overhead, inevitable bugs, and inability to write certain containers/algorithms safely with exceptions just isn't worth supporting a rather mediocre error handling facility. Fatal error reporting (asserts), monadic errors, etc. solve more problems while introducing fewer new ones.

At the very least, enforce noexcept on your move operations. Or even do so on copy; you shouldn't be copying containers or anything that needs to allocate on copy (I'd even argue strongly that containers shouldn't even have copy operations but rather only fill-from operations).

 

 

Here I'm gonna have to disagree.  I love exceptions and copying is essential for many fundamental operations/algorithms.  I do think C++'s handling of copying/movings and their interactions with exceptions is a bit problematic, but that's a conversation for another thread...

Share this post


Link to post
Share on other sites

Under C++11 POD's can have trivial ctor's & dtor's. I think you can even inherit (with restrictions).

 

Practically speaking you just can't do anything that results in the class having a vtable.

 

This has all worked for years it just was guaranteed by the standard.

Edited by Shannon Barber

Share this post


Link to post
Share on other sites
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!