Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Xml improvements #831

Merged
merged 12 commits into from
Mar 15, 2021
Merged

Xml improvements #831

merged 12 commits into from
Mar 15, 2021

Conversation

Brett208
Copy link
Member

Must be paired with assets update to compile: OutpostUniverse/ophd-assets#1

It looks like the assets are a submodule of this repo, but I had trouble linking them on my machine for some reason.

Related to #830. Not quite a string dictionary but tried to consolidate some of the xml related code.

The last commit is pretty big. I had trouble teasing it into smaller chunks due to the serialization involved. I was experimenting with template functions and enum lookups. Hopefully it looks ok...

This PR will allow someone to change the planet attributes without recompiling the source code.

OPHD/States/Planet.cpp Outdated Show resolved Hide resolved
OPHD/States/Planet.cpp Outdated Show resolved Hide resolved
OPHD/States/Planet.cpp Outdated Show resolved Hide resolved
OPHD/States/Planet.cpp Outdated Show resolved Hide resolved
OPHD/XmlSerializer.h Outdated Show resolved Hide resolved
OPHD/XmlSerializer.h Outdated Show resolved Hide resolved
@ldicker83
Copy link
Collaborator

Looked over it. Generally no objections, looks pretty good. I made a few notes of areas where brace placement is inconsistent with the rest of the code. With that corrected this PR should be good to go.

Copy link
Member

@DanRStevens DanRStevens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really like the idea of moving that data to a data file.

I had a few suggestions and observations looking at the last commit, though they were all pretty minor.

OPHD/Common.cpp Outdated Show resolved Hide resolved
OPHD/XmlSerializer.cpp Show resolved Hide resolved
OPHD/States/Planet.h Outdated Show resolved Hide resolved
OPHD/States/Planet.h Outdated Show resolved Hide resolved
OPHD/XmlSerializer.cpp Outdated Show resolved Hide resolved
OPHD/XmlSerializer.h Show resolved Hide resolved
OPHD/XmlSerializer.h Outdated Show resolved Hide resolved
Comment on lines 86 to 95
void parseElementValue(Planet::PlanetType& destination, const NAS2D::Xml::XmlElement* element)
{
destination = stringToEnum(Planet::planetTypeTable, element->getText());
}


void parseElementValue(Planet::Hostility& destination, const NAS2D::Xml::XmlElement* element)
{
destination = stringToEnum(Planet::hostilityTable, element->getText());
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of feel like destination should be a return value, rather than an out parameter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably a premature optimization. I want to move some large strings (definitions of the planets) into the xml file in the near future and I figured we may not want to pass large chunks of data in the return.

I'll make this a return for now as suggested and we can revisit if anyone ever notices a slowdown...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EDIT: I was attempting to keep function names the same between parsing an enum and parsing a basic value. Since type deduction is not possible based on return values of a function, I think using a return value here isn't possible. The alternative if we want a return value would be to provide separate names for each function. I was hoping to keep function names the same to possibly allow collapsing if/else statement logic cases in future iterations.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As of C++17, the compiler is guaranteed to do the optimization for you:
https://en.cppreference.com/w/cpp/language/copy_elision

When returning large objects, the compiler will effectively pass in a hidden parameter with the address of the variable being copied to, and will instead construct the value in place. Hence no copy during the return.


You can potentially use template methods to parameterize the return type. If the functions have different implementations, this can be handled with template specialization. Though often, just giving different functions different names may be easier and sometimes makes more sense. I'd need to look over the code again to consider this.

OPHD/XmlSerializer.h Outdated Show resolved Hide resolved
@Brett208
Copy link
Member Author

The unnamed namespace work is a bit hairy due to the underlying structure. There are really 2 different sets of information being represented in Planet.h (the attributes of a planet and how to draw/manipulate in the UI a planet on screen). That logic is partially split between PlanetSelectState and Planet, making it even more complex. I think disentagling that is probably not a good idea in this thread though.

Made the easier style changes in addition to the anonymous namespace. I'll work the other points later.

Thanks for the thorough and quick feedback.

@Brett208
Copy link
Member Author

I wasn't thrilled with the results of adding the anonymous namespace. Having to prepend the :: qualifier to some of the functions may cause noise later.

I am complete with updates unless the additions I made need further refinement before merging.

Copy link
Member

@DanRStevens DanRStevens left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is good to merge. I just want to take a bit of a closer look at the namespace scope issue, and why :: is needed in a few places. Maybe there's another workaround.

Comment on lines +133 to +136
namespace
{
Planet::Attributes parsePlanet(const NAS2D::Xml::XmlElement* xmlNode)
{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that it matters, though you could potentially have put this in the first unnamed namespace, possibly even replacing the function prototype. Though there is nothing wrong about re-opening an unnamed namespace and keeping the implementation separate.

Comment on lines +145 to +152
if (element->value() == "PlanetType")
{
parseElementValue(attributes.type, element);
}
else if (element->value() == "ImagePath")
{
::parseElementValue(attributes.imagePath, element);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, yes, I see the discrepancy with the ::. It seems you have two functions parseElementValue, defined in different namespaces. That could get confusing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the review. My goal was to keep the function names the same. I reasoned it might be possible to use a table to match argument names to locations in the structure to remove the if / else logic statements to place each value. I thought doing this would be easier if function overloads / templates were used. It gets complicated because of the detailed knowledge required about the enum to parse it properly, so the local module will need to define overloads for which enums it wants to support parsing. If this seems silly, I'm OK if the enum parse functions have custom names...

I think it might be worth trying to limit enums when possible when parsing to simplify. We could just represent hostility as a scale of 1 to 10 using an int value instead of 'low' 'medium' 'high' for example which would cut out the custom parsing logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is the different attributes have different types, and selecting which function overload or template instantiation to call is a compile time thing. It's not compatible with a runtime loop. Trying to use a table would require some kind of compile time iteration.

One of the things I was thinking about was being able to map an enum type to the lookup table used to map strings to enum values. It is possible to use templates to map types to values:
https://gpfault.net/posts/mapping-types-to-values.txt.html

I was thinking if we used such a solution, we could update the stringToEnum function to not require the first parameter:

template<typename T>
T stringToEnum(const std::unordered_map<std::string, T>& table, std::string value)

Instead, the table could be looked up internally using such a mapping template on the template parameter.

Taking that idea further, it could be combined with the parseElementValue function:

template<typename T, std::enable_if_t<!std::is_enum<T>::value, bool> = true>
void parseElementValue(T& destination, const NAS2D::Xml::XmlElement* element)

You could potentially remove the std::enable_if guard, and instead use a if constexpr to select whether to call parseElementValue or stringTo. And you might as well, since there would no longer be an external table parameter to pass in, so it would be very easy to call in such a context.

Would then make it easy to use a return value, rather than an out parameter.

Of course, that all hinges on being able to map a type to a value. I gave it a bit of a try, though if you define the type mapping in a different namespace than it's used, that seems to cause issues.


// Specialize this for each EnumType with a lookup table (not shown)
template <typename EnumType>
struct EnumValueName { static const std::unordered_map<std::string, EnumType> table; };

template<typename EnumType>
T stringToEnum(std::string value)
{
    const auto& table = EnumValueName<EnumType>::table;
    // Old implementation ...
}

template<typename T>
T parseElementValue(const NAS2D::Xml::XmlElement* element)
{
    try 
    {
        const auto value = element->getText();
        if constexpr (std::is_enum_v<T>)
        {
            return stringToEnum<T>(value);
        }
        else
        {
            return NAS2D::stringTo<T>(value);
        }
    }
    // Old implementation ...
}

@DanRStevens
Copy link
Member

Yeah, maybe I'll look into that as a post-merge thing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants