Using different variables based on template type
I have a couple of functions that provide the same functionality for different types. I'd like to combine them to reduce code duplication. An example of two functions is listed below. std::map<std::string, uint16> m_Uint16; std::map<std::string, uint32> m_Uint32; void putUint16(std::string const& variableName, uint16 const& value) { m_Uint16[variableName] = value; } void putUint32(std::string const& variableName, uint32 const& value) { m_Uint32[variableName] = value; } The functions can't be combined into a template since they are the interface for the programmer. Is there a boost library that would let me call a template or common function that would use a different variable based on the variable type? Ryan
2009/6/1 Ryan McConnehey <mccorywork@gmail.com>:
I have a couple of functions that provide the same functionality for different types. I'd like to combine them to reduce code duplication. An example of two functions is listed below.
std::map<std::string, uint16> m_Uint16; std::map<std::string, uint32> m_Uint32;
void putUint16(std::string const& variableName, uint16 const& value) { m_Uint16[variableName] = value; }
void putUint32(std::string const& variableName, uint32 const& value) { m_Uint32[variableName] = value; }
The functions can't be combined into a template since they are the interface for the programmer. Is there a boost library that would let me call a template or common function that would use a different variable based on the variable type?
If you need those function names, I think you're stuck with a macro, since a template can't generate an identifier based on an argument. #define NASTY_PREFIX_HERE_PUT_FOR_TYPE(T) \ void put_##T(std::string const& variableName, T const& value) { \ m_##T[variableName] = value; \ } \ std::map<std::string, T> m_##T NASTY_PREFIX_HERE_PUT_FOR_TYPE(uint16); NASTY_PREFIX_HERE_PUT_FOR_TYPE(uint32); If you can change the API, then you could use a template like this: template <typename T> class foo { // can't think of a good name std::map<std::string, T> m; public: void put(std::string const& variableName, T const& value) { m[variableName] = value; } }; Then publically inherit from foo<uint16> and foo<uint32> (I think there's an inherit_linearly or something that would help, given a typelist, though it might have hiding problems). If you dislike overload resolution, I think you can do something like this: whatever.foo<uint16>::put("yay", 0x1234); Not certain about the syntax there, though. Or add a template function to do the dispatch to the correct base, allowing syntax like: whatever.put<uint16>("boo", 0x4321); Since you likely need a matching get, for which overload resolution is less nice, the last version is probably my favourite. Good luck, ~ Scott
2009/6/1 Ryan McConnehey <mccorywork@gmail.com>:
Where would you rank this solution?
[...]
That still requires 2 copies, 2 pastes, and 4 edits to add a new type (PLUS those for the other functions). They're at least simple, but it's still a violation of DRY, so I'm not a big fan. If you decided it's more readable than the other options, then I suppose I could go for it if the number of types was quite low. What are the types really, anyways? Differentiating between uint16 and uint32 as in the example is rarely useful. Certainly on a normal desktop, the map nodes will end up being the same size anyways, thanks to padding.
Scott McMurray wrote:
They're at least simple, but it's still a violation of DRY, so I'm not a big fan.
If a non-expanding interface is needed (non-template so that every type isn't accepted) does it really violate DRY?
What are the types really, anyways? I've create true_typedef of the basic type like uint16, uint32, int16, etc. I've written my own serialization interface (I know boost already has one) and I'm restricting the interface to only allow certain types. I'm trying to centralize the were the real work happens so if new types are needed then added them won't be difficult.
Ryan
On Tue, Jun 2, 2009 at 2:31 AM, Ryan McConnehey <mccorywork@gmail.com>wrote:
Scott McMurray wrote:
They're at least simple, but it's still a violation of DRY, so I'm not a big fan.
If a non-expanding interface is needed (non-template so that every type isn't accepted) does it really violate DRY?
What are the types really, anyways?
I've create true_typedef of the basic type like uint16, uint32, int16, etc. I've written my own serialization interface (I know boost already has one) and I'm restricting the interface to only allow certain types. I'm trying to centralize the were the real work happens so if new types are needed then added them won't be difficult.
Is there any reason you have to have different names for the map? Use template specialization to achieve the same thing as name resolution for you but still allowing you to access the maps through a consistent naming scheme. //The default specialization will cause compilation to fail unless DECL_SERIALIZATION_SUPPORT(T) has been called for a type #define BEGIN_SERIALIZATION_DECLS() \ namespace serialization { \ template<typename T> struct kv_map { }; \ #define DECL_SERIALIZATION_SUPPORT(T) \ template<> \ struct kv_map<T> \ { \ typedef std::map<std::string, T> map_type; \ map_type map; \ }; \ kv_map<T>::map_type map; #define END_SERIALIZATION_DECLS() } template<typename T> void store_kv(const std::string& key, const T& value) { serialization::kv_map<T>::map[key] = value; } template<typename T> T& retrieve_kv(const std::string& key) { return serialization::kv_map<T>::map[key]; } now you don't even need the put function at all anymore, and you can invoke it like this: BEGIN_SERIALIZATION_DECLS() DECL_SERIALIZAITON_SUPPORT(boost::uint16_t) DECL_SERIALIZATION_SUPPORT(boost::uint32_t) END_SERIALIZATION_DECLS() boost::uint16_t test1 = 7; boost::uint32_t test2 = 8; store_kv("test1", test1); store_kv("test2", test2); test1 = retrieve_kv<boost::uint16_t>("test1"); test2 = retrieve_kv<boost::uint32_t>("test2"); If you need to provide different storage / retrieval logic for different types then just specialize the store_kv and retrieve_kv functions. When you want to support a new type you only add 1 line of code, a DECL_SERIALIZATION_SUPPORT macro. I used a very similar method to this to provide compile-time lookup of the names of the different fields in an enum and it works well.
BEGIN_SERIALIZATION_DECLS() DECL_SERIALIZAITON_SUPPORT(boost::uint16_t) DECL_SERIALIZATION_SUPPORT(boost::uint32_t) END_SERIALIZATION_DECLS() I like how easy this is to expand for new types. A requirement that didn't seem important at the time, but now may affect this design, is having multiple variable names at different levels. For example, within
Zachary Turner wrote: the level (the levels are only one deep)"Bob" and "Sue" the variable "test1" can exist. They are not required to be the same type though. Since types can change I remove the variable name before I store the new variable at the given level. This means current I have a structure with all my different type maps and then a map of levels (level names as the key) and type structures. I have a shared_ptr, to the current level structure of types, and all inputed variables name and values are placed within the shared_ptr structure. I don't think the design would need to change but do you see any problem?
If you need to provide different storage / retrieval logic for different types then just specialize the store_kv and retrieve_kv functions. When you want to support a new type you only add 1 line of code, a DECL_SERIALIZATION_SUPPORT macro. If the storage / retrieval logic is customized for different types, how is this only adding one line for a new type? Don't you have to add the one line and the extra storage / retrieval logic for the new type?'
Ryan
On Tue, Jun 2, 2009 at 10:19 AM, Ryan McConnehey <mccorywork@gmail.com>wrote:
Zachary Turner wrote:
BEGIN_SERIALIZATION_DECLS() DECL_SERIALIZAITON_SUPPORT(boost::uint16_t) DECL_SERIALIZATION_SUPPORT(boost::uint32_t) END_SERIALIZATION_DECLS()
I like how easy this is to expand for new types. A requirement that didn't seem important at the time, but now may affect this design, is having multiple variable names at different levels. For example, within the level (the levels are only one deep)"Bob" and "Sue" the variable "test1" can exist. They are not required to be the same type though. Since types can change I remove the variable name before I store the new variable at the given level. This means current I have a structure with all my different type maps and then a map of levels (level names as the key) and type structures. I have a shared_ptr, to the current level structure of types, and all inputed variables name and values are placed within the shared_ptr structure.
I don't think the design would need to change but do you see any problem?
First thing is I actually made a slight mistake in my code example, the line "map_type map;" inside the struct declaration should have been "static map_type map;" Regarding your other question, I'm not totally sure I understand, could you post a short code snippet? If you mean that you want different maps for the same type, for example: std::map<std::string, boost::uint16_t> uint16map_1; std::map<std::string, boost::uint16_t> uint16map_2; std::map<std::string, boost::uint16_t> uint16map_3; Then the design I mentioned doesn't support this directly, but I think it could be made to support it a number of different ways. The easiest way would be to just add an additional argument to the BEGIN_SERIALIZATION_DECLS() macro and use that argument for the namespace name instead of "serialization". Then you would have: BEGIN_SERIALIZATION_DECLS(serialization_map1) DECL_SERIALIZATION_SUPPORT(boost::uint16_t) DECL_SERIALIZATION_SUPPORT(boost::uint32_t) END_SERIALIZATION_DECLS() BEGIN_SERIALIZATION_DECLS(serialization_map2) DECL_SERIALIZATION_SUPPORT(boost::uint32_t) DECL_SERIALIZATION_SUPPORT(foo) END_SERIALIZATION_DECLS() serialization_map1::kv_map<boost::uint16_t>::map["test1"] = 7; serialization_map2::kv_map<boost::uint16_t>::map["test1"] = 8;
If you need to provide different storage / retrieval logic for different
types then just specialize the store_kv and retrieve_kv functions. When you want to support a new type you only add 1 line of code, a DECL_SERIALIZATION_SUPPORT macro.
If the storage / retrieval logic is customized for different types, how is this only adding one line for a new type? Don't you have to add the one line and the extra storage / retrieval logic for the new type?'
Sorry, I meant that for adding support for types which didn't need custom storage / retrieval is just 1 line, because then all you do is do DECL_SERIALIZATION_SUPPORT. yea if you need custom storage / retrieval then it's 1 line for the DECL_ macro and N lines for the storage / retrieval logic. You can do them in the form of template specializations for the type of data to store/retrieve, or if you already have the functions implemented somewhere else with different names that can't easily be adapted to template specializations you could store a static boost::function object in the kv_map specialization and take it as an argument to the macro. It will probably take some tinkering with to settle on something but I think the basic idea can be adapted. OTOH, macros can get pretty hairy the more complicated you make them, so if that's a consideration you might consider something else. You'd only want to do something like this if you didn't think you'd end up needing to modify the the actual macros very much, if at all, once you had them in place.
Regarding your other question, I'm not totally sure I understand, could you post a short code snippet? class Serialization {
Zachary Turner wrote: private: struct DataStructure { std::map<std::string, uint16> m_Uint16Map; std::map<std::string, uint32> m_Uint32Map; void remove (std::string const& variableName) { m_Uint16Map.erase(variableName); m_Uint32Map.erase(variableName); } }; typedef boost::shared_ptr<DataStructure> boostData; std::map<std::string, boostData> m_DataMap; boostData m_CurrentStructure; public: Serialization(void) { m_CurrentStructure = boostData(new DataStructure); m_DataMap[""] = m_CurrentStructure; } void putUint16(std::string const& variableName, uint16 variable) { m_CurrentStructure.remove(variableName); m_CurrentStructure->m_Uint16Map[variableName] = variable; } void putUint32(std::string const& variableName, uint32 variable) { m_CurrentStructure.remove(variableName); m_CurrentStructure->m_Uint32Map[variableName] = variable; } };
You can do them in the form of template specializations for the type of data to store/retrieve If I use template specialization I would be creating a function for every type. This doesn't seem any better than creating a function for each type I want. Why would I want to do this?
Ryan
2009/6/2 Ryan McConnehey <mccorywork@gmail.com>:
Scott McMurray wrote:
They're at least simple, but it's still a violation of DRY, so I'm not a big fan.
If a non-expanding interface is needed (non-template so that every type isn't accepted) does it really violate DRY?
Sure. Just 'cause it's templated doesn't mean it'll accept anything. In the inheriting version, for example, the delegating to a type-specific base class keeps it from accepting "anything".
What are the types really, anyways?
I've create true_typedef of the basic type like uint16, uint32, int16, etc. I've written my own serialization interface (I know boost already has one) and I'm restricting the interface to only allow certain types. I'm trying to centralize the were the real work happens so if new types are needed then added them won't be difficult.
So you want a way where you have an MPL typelist of the types, and just adding a type to that typelist is all you need to do. (Or to a PP sequence, or ...)
Scott McMurray wrote:
So you want a way where you have an MPL typelist of the types, and just adding a type to that typelist is all you need to do. (Or to a PP sequence, or ... Being able just add a type to a typelist and then have the appropriate helper functions generated would be nice but not a necessary. So far I'm having to put some logic functions into my helper functions.
void putUint16(std::string const& variableName, uint16 const variable) { m_CurrentStructure->remove(variableName); //This function call is duplicated in every helper function. m_CurrentStructure->m_Uint16Map[variableName] = variable; //This is needed in every helper function but the appropriate map is needs to be used. } Since all these helper functions are exactly the same except the map being called I was hoping to refactor to allow adding new type more easily. By the way, what is a PP sequence? Ryan
2009/6/2 Ryan McConnehey <mccorywork@gmail.com>:
Being able just add a type to a typelist and then have the appropriate helper functions generated would be nice but not a necessary. So far I'm having to put some logic functions into my helper functions.
void putUint16(std::string const& variableName, uint16 const variable) { m_CurrentStructure->remove(variableName); //This function call is duplicated in every helper function. m_CurrentStructure->m_Uint16Map[variableName] = variable; //This is needed in every helper function but the appropriate map is needs to be used. }
Since all these helper functions are exactly the same except the map being called I was hoping to refactor to allow adding new type more easily.
And as mentioned, the "which map" issue can be handled in templates by dispatching to an appropriate base class.
By the way, what is a PP sequence?
The boost preprocessor library would allow you do do something like this: UGLY_PREFIX_ALLOW_TYPES( (uint16)(uint32)(guid)(templated<something>) ); and auto-generate the macro invocations UGLY_PREFIX_ALLOW_TYPE(uint16); UGLY_PREFIX_ALLOW_TYPE(uint32); and so on.
template <typename T> void put(std::map<std::string, T> const& map, std::string const& variableName, T const& value) { map[variableName] = value; }
void putUint16(std::string const& variableName, uint16 const& value) { put(m_Uint16, variableName, value); }
void putUint32(std::string const& variableName, uint32 const& value) { put(m_Uint32, variableName, value); }
This is the correct solution. Andrew Sutton andrew.n.sutton@gmail.com
participants (4)
-
Andrew Sutton
-
Ryan McConnehey
-
Scott McMurray
-
Zachary Turner