-
Notifications
You must be signed in to change notification settings - Fork 22
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
RSDK-8541: ProtoValue get_unchecked and visit API #279
Conversation
…ure/proto-value-visit
There was a problem hiding this 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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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 atemplate
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 aProtoValue
of some unrelated type. -
Should (some of the)
ProtoValue
constructors (templated or not) beexplicit
? -
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 letstd::make_shared<ProtoType>(5)
still work, I think. What I want to avoid is confusing situations where by acceptingint
as a constructor argument, we allow things with conversion toint
to be passed, but then those things become doubles internally.
There was a problem hiding this comment.
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 privatetemplate
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 exposekind_t
and then rebase this one off that one?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
src/viam/sdk/common/proto_value.hpp
Outdated
/// 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; |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point!
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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@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 |
There was a problem hiding this 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).
src/viam/sdk/common/proto_value.hpp
Outdated
/// 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; |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
src/viam/sdk/common/proto_value.hpp
Outdated
class ProtoValue { | ||
public: | ||
/// @brief Type discriminator constants for possible values stored in a ProtoValue. | ||
enum Kind { null, bool_, int_, double_, string, list, struct_ }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- We usually prefix constants with
k_
(google style thing, but one I like), sok_null
,k_bool
will all work I think. - I think maybe you should give these explicit numeric values since they form part of the interface.
- Maybe make the order the same order as the struct proto does: https://github.com/protocolbuffers/protobuf/blob/main/src/google/protobuf/struct.proto#L66-L76. Throw
int
at the end since we expect to remove it.
There was a problem hiding this comment.
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
Adds an
unchecked_get
API to get the stored value in aProtoValue
, and avisit
API in the spirit ofstd::visit
or similar.Tests both
unchecked_get
andvisit
. Note that theProtoValue
tests have been moved into their own source file now. In this diff all that's new intest_proto_value.cpp
is theget_unchecked
tests. Thevisit
tests are in their own test source file.