Controlling the Output Format of Specific Types
I mentioned earlier that all that was needed to support a given type as an Attribute is that the input and output stream operators exist for that type.
While this is true, relying on the standard operators for default types may not give you the results you expect or want. C++ stream operators provide several manipulators and output format controls for controlling the output of a variable to the stream.
You might, for example, want your floating point types to output in fixed notation rather than scientific. You do not have to sacrifice this ability when using Attributes.
Using partial template specialization you can override the default serialization for any type of object. Essentially, output to a stream is accomplished by first converting the object to a string representation through a call to the ‘ToString’ method of the Attribute class.
Input is done the same way by calling FromString. Providing specialized versions of these methods gives you the means to completely control the format of types which have already implemented the << and >> stream operators.
Here is the template specialization for ‘Attribute<double>::ToString()’:
Listing 6
template <>
_string Attribute<double>::ToString()
{
_ostringstream s;
if ( m_pData )
s << setiosflags(std::ios::fixed) << *m_pData;
return s.str();
}
You may notice in the included source code that template specializations are also provided for both the ToString and FromString methods of the default string type. This is done as a performance enhancement to avoid using stringstream to ‘convert’ std::string objects.
Possible Changes
The included library uses a policy pattern for implementing
the IO of the AttributeContainer. One drawback to this pattern is that it does
not provide for different IO mechanisms. As written, if you want to serialize
to more than one type of medium, you have to use different
AttributeContainers.
One solution around
this problem is to use the strategy pattern
instead. Policies are compile-time bound, whereas strategies can be changed at
run-time. I leave this implementation up
to you. You can find more information on strategies in any number of books on
patterns.
Performance
Considerations
It is always important to know your tools and how they will
impact the overall performance characteristics of your software. Attribute and
AttributeContainer, while they are very useful and convenient tools, do have
some important considerations regarding memory.
On the Windows operating system, using Visual Studio, Attribute
uses 52 bytes of memory in debug, and 44 in release. Since Attribute stores
only a pointer to the variable, the size of the variable type is irrelevant.
AttributeContainer uses 36 bytes. It is important to note however, that these
values can be significantly reduced with some careful thought.
The version of the library that accompanies this article was
written to be easily understandable and easily expanded. It is not optimized
for memory use. Here are some suggestions to optimize the memory use to better
suit your specific needs:
-
Change
the type of the variable used to hold the behavior flags in Attribute from the
default unsigned long (4 bytes) to a short int (2 bytes) or unsigned char (1
byte). This will reduce the total number of flags that your library can
support, but once you know how many flags you need you can reduce this variable
to a more reasonable size.
-
Change
the type of the string object to char* or to a string class with a smaller
memory footprint than std::string. Keep in mind that while std::string seems
excessive in its memory footprint, many implementations of it use a shared
string manager to reduce the overall memory requirements. 32 bytes of the total
memory used by Attribute is used by std::string.
-
Consider
removing the ‘bind order' variable entirely. AttributeContainer keeps track of
the order in which Attributes are bound so that it can write them out to xml or
otherwise list them in the same order. This is a convenience that can likely be
lived without in environments with rigid memory requirements. This is another
candidate for an #ifdef. It might be useful in your UI editor to list all of
the Attributes in the order they were bound, but you may not need that
functionality in your release code.
-
Consider
using a Proxy object rather
than having your object inherit directly from AttributeContainer. In this case,
you will most likely be swapping memory for performance issues.
|
After rereading the article, I see you mentioned this refactor potential:
"One alternative is to have a global instance of AttributeContainer (or at least one that is in a larger scope than the objects it manages). While this saves some memory, it does complicate use, as each bound attribute must be somehow uniquely identified per object."
There are certainly some not-too-difficult solutions that simplify the global shared container pattern for client code.
In a system I worked with, the following client code would serialize an object's properties to a stream:
Object *pObject = ...;
MutableString mutstr;
StringOutputStream sos(mutstr);
XmlSerializer ser(os);
ser.Serialize(pObject);