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

RSDK-8541: ProtoValue get_unchecked and visit API #279

Merged
merged 74 commits into from
Sep 20, 2024

Conversation

lia-viam
Copy link
Contributor

Adds an unchecked_get API to get the stored value in a ProtoValue, and a visit API in the spirit of std::visit or similar.

Tests both unchecked_get and visit. Note that the ProtoValue tests have been moved into their own source file now. In this diff all that's new in test_proto_value.cpp is the get_unchecked tests. The visit tests are in their own test source file.

@lia-viam lia-viam requested a review from a team as a code owner August 14, 2024 20:03
@lia-viam lia-viam requested review from njooma, stuqdog and acmorrow and removed request for a team August 14, 2024 20:03
Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

LGTM, but please address the things I raised here before merging. If you want me to take another look before merging just re-request my review, otherwise, I don't think another round of review is required unless there are substantive changes required to address any of my comments. I don't think there will be.


template <>
struct kind_t<int> : std::integral_constant<int, 2> {};
struct kind<int> {
Copy link
Member

Choose a reason for hiding this comment

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

It just occurred to me that proto's struct doesn't actually have an integer type. Just double. Do we want to be in the business of defining that conversion, or should we push that to users, since we cannot always know their intent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've been wondering about this too--proto just has a number type which promotes everything to double. There are some gotchas in the unit tests about this:
https://github.com/lia-viam/viam-cpp-sdk/blob/feature/proto-value-visit/src/viam/sdk/tests/test_proto_value.cpp#L43

Copy link
Member

Choose a reason for hiding this comment

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

Having mulled it over, I think it should not have an int case, or conversion functions from int. It gives the user the illusion that int is supported, but it really isn't, since it becomes a double. If anything, I'm almost tempted to say that we should =delete the ability to construct ProtoValue from integers, so that the caller must deal with the conversion themselves.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm in favor of getting rid of the separate int and double types and replacing them with a number type which is just a double underneath, as this would match what's in proto
https://protobuf.dev/reference/protobuf/google.protobuf/#value

Deleting the conversion from int entirely is tempting, but maybe this will cause us headaches when migrating from ProtoType? The variant underlying that class also has distinct int and double, which is arguably a mistake for the same reason, but there will definitely be instances of std::make_shared<ProtoType>(5)

Copy link
Member

Choose a reason for hiding this comment

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

OK, so several thoughts have come from this discussion:

  • Does ProtoValue really need a template constructor, or should it just have constructors for each type. As written, you will just get a funky probably-link-time error because it can't instantiate it if you try to, say, construct a ProtoValue of some unrelated type.

  • Should (some of the) ProtoValue constructors (templated or not) be explicit?

  • I suspect that if we eliminate the ability to hold integers, then the one numeric constructor should take double, but allow implicit conversions (so, not explicit). That'd let std::make_shared<ProtoType>(5) still work, I think. What I want to avoid is confusing situations where by accepting int as a constructor argument, we allow things with conversion to int to be passed, but then those things become doubles internally.

Copy link
Contributor Author

@lia-viam lia-viam Sep 18, 2024

Choose a reason for hiding this comment

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

All good questions. My thoughts

  • I may as well just spell out a bunch of non-template constructors for each type, although I think I would still want to have them defer to a private template constructor. Whatever amount of duplication is present it would still be fewer lines than all the explicit instantiations I'm writing
  • I think yeah ideally we would want to follow the example of JSON libraries, where you have sort of easy ergonomic interfaces for building up objects like this, which you get because the constructors are not explicit.
  • Yeah I think this makes sense and we will just be deferring to standard C++ language rules about type promotion/argument overloading

All that said, maybe I should make a separate PR to do

  • have only number type
  • define non-template public constructors
  • make kind an enum and don't expose kind_t

and then rebase this one off that one?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, let's go forward with what you have now where int is a thing, and then defer the whole int removal and any associated constructor fussing into a followup PR. I don't think you need to do the rebase part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK cool so just to confirm should I still do the kind_t removal and changing to an enum Kind for this PR too?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think the kind_t removal is pretty trivial, and I'd really like to see the default case go away if possible, so I think the enum thing makes more sense in this review than in a new one.

/// This type trait can be used to induce substitution failure on non-ProtoValue constructible
/// types.
template <typename T>
using kind_t = typename proto_value_details::kind<T>::type;
Copy link
Member

Choose a reason for hiding this comment

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

Do you need the _t?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I suppose not, more just convention

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually just tried doing this change, this made the compiler complain because of the ambiguity with the kind() method, I think we might be better off keeping the _t, or possibly using capital Kind, actually not super clear what our convention is here as I think it varies a bit throughout the SDK

Copy link
Member

Choose a reason for hiding this comment

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

Actually, do you even need kind_t? Is there a reason not to just say proto_value_details::kind<T>::type where you otherwise would have said kind_t? I don't see that kind_t figures in the public API of ProtoValue anywhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point!

src/viam/sdk/common/proto_value_visit.hpp Show resolved Hide resolved
return std::forward<Visitor>(visitor)(value.get_unchecked<std::vector<ProtoValue>>());
case kind_t<ProtoStruct>{}:
return std::forward<Visitor>(visitor)(value.get_unchecked<ProtoStruct>());
default:
Copy link
Member

Choose a reason for hiding this comment

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

Is a default needed at all? Should it result in calling the nullptr case for visitor? I feel like it shouldn't be possible or should be treated as some sort of fatal error.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's "needed" just to not make the compiler complain, but in theory it's not possible by the invariants we maintain on the proto value objects. We could do a BOOST_UNREACHABLE_RETURN or similar.

Copy link
Member

Choose a reason for hiding this comment

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

Would having kind return an enum over the available types placate the compiler about not having a default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think that would work actually, I'll give it a shot

src/viam/sdk/tests/test_proto_value.cpp Outdated Show resolved Hide resolved
src/viam/sdk/tests/test_proto_value_visit.cpp Outdated Show resolved Hide resolved
src/viam/sdk/common/proto_value.cpp Show resolved Hide resolved
src/viam/sdk/common/proto_value.cpp Outdated Show resolved Hide resolved
@lia-viam
Copy link
Contributor Author

@acmorrow just to sum up for re-review, addressed most of the comments, further decision probably needed on the "number" type and on if we want to put unreachable in our switch statements, which also affects existing code.

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

This looks great. I'm really happy with how little implementation lives in proto_value.hpp now, and the new visitation functions look great.

I think the only things left to do are:

  • See if it is possible to eliminate the default case
  • Decide about how to handle int (personal preference to disallow it and make the caller figure it out).

/// This type trait can be used to induce substitution failure on non-ProtoValue constructible
/// types.
template <typename T>
using kind_t = typename proto_value_details::kind<T>::type;
Copy link
Member

Choose a reason for hiding this comment

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

Actually, do you even need kind_t? Is there a reason not to just say proto_value_details::kind<T>::type where you otherwise would have said kind_t? I don't see that kind_t figures in the public API of ProtoValue anywhere.

return std::forward<Visitor>(visitor)(value.get_unchecked<std::vector<ProtoValue>>());
case kind_t<ProtoStruct>{}:
return std::forward<Visitor>(visitor)(value.get_unchecked<ProtoStruct>());
default:
Copy link
Member

Choose a reason for hiding this comment

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

Would having kind return an enum over the available types placate the compiler about not having a default?


template <>
struct kind_t<int> : std::integral_constant<int, 2> {};
struct kind<int> {
Copy link
Member

Choose a reason for hiding this comment

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

Having mulled it over, I think it should not have an int case, or conversion functions from int. It gives the user the illusion that int is supported, but it really isn't, since it becomes a double. If anything, I'm almost tempted to say that we should =delete the ability to construct ProtoValue from integers, so that the caller must deal with the conversion themselves.

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

This LGTM mod some suggested / minor tweaks to the new enum. No need to do another round of review for those changes unless you want to.

I like the new typedef for ListValue too.

class ProtoValue {
public:
/// @brief Type discriminator constants for possible values stored in a ProtoValue.
enum Kind { null, bool_, int_, double_, string, list, struct_ };
Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sounds good, going to do all these except keep the same order we have (but with int thrown at the end still). The reason for the proto sequencing escapes me, and I think it's easier mnemonically to write switch statements and sequence the methods if it goes from least to most data-rich types which is also what you see, eg, in JSON library APIs usually

@lia-viam lia-viam merged commit 7d48705 into viamrobotics:main Sep 20, 2024
3 checks passed
@lia-viam lia-viam deleted the feature/proto-value-visit branch September 20, 2024 18:24
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.

2 participants