-
Notifications
You must be signed in to change notification settings - Fork 465
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
Mark methods of wrapper classes const #874
Conversation
Help, lint is complaining about lines I didn't touch. |
Tagged for discussion in the N-API team meeting this week: nodejs/abi-stable-node#418 I still wonder about const even when if effectively mutates internal state and want to get more team discussion to see what people think. |
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 this PR takes advantage of the fact that napi_value
is a plain pointer. If we were to store V8 objects (like v8::Local<v8::Value>
) directly inside Napi::*
classes instead of the intermediate napi_value
objects, which (if any) functions would we be able to mark as const?
For example, I don't think we could render Napi::Object::Set
as const
.
I think if we mark any of these functions as const we should keep the underlying concepts in mind.
@@ -442,7 +442,7 @@ inline Value Env::RunScript(String script) { | |||
|
|||
#if NAPI_VERSION > 5 | |||
template <typename T, Env::Finalizer<T> fini> | |||
inline void Env::SetInstanceData(T* data) { | |||
inline void Env::SetInstanceData(T* data) const { |
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 does conceptually modify the environment, because it attaches instance data to it. I know it doesn't modify the state of the Napi::Env
object, but it does modify the state of napi_env
.
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.
If attaching instance data to the environment is conceptually modifying the Env object, then this modification shouldn't affect copies of the object. But copies of Env share the same environment, similarly to how copies of shared_ptr point to the same object.
@@ -453,7 +453,7 @@ inline void Env::SetInstanceData(T* data) { | |||
template <typename DataType, | |||
typename HintType, | |||
Napi::Env::FinalizerWithHint<DataType, HintType> fini> | |||
inline void Env::SetInstanceData(DataType* data, HintType* hint) { | |||
inline void Env::SetInstanceData(DataType* data, HintType* hint) const { |
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.
Same here.
napi-inl.h
Outdated
@@ -1234,50 +1222,50 @@ inline Value Object::Get(const std::string& utf8name) const { | |||
} | |||
|
|||
template <typename ValueType> | |||
inline void Object::Set(napi_value key, const ValueType& value) { | |||
inline void Object::Set(napi_value key, const ValueType& value) const { |
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.
As with the above, conceptually, Object::Set
does modify the value.
napi-inl.h
Outdated
Set(utf8name.c_str(), value); | ||
} | ||
|
||
inline bool Object::Delete(napi_value key) { | ||
inline bool Object::Delete(napi_value key) const { |
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.
Same for Object::Delete
.
napi-inl.h
Outdated
@@ -1316,19 +1304,19 @@ inline Array Object::GetPropertyNames() const { | |||
return Array(_env, result); | |||
} | |||
|
|||
inline void Object::DefineProperty(const PropertyDescriptor& property) { | |||
inline void Object::DefineProperty(const PropertyDescriptor& property) const { |
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.
... and the same for these.
All of them I think, since you can access the non-const inner object from a const Local: https://v8docs.nodesource.com/node-15.0/de/deb/classv8_1_1_local.html#aca5a4236c43b7b3bc530c6bb88cf9272 |
We discussed that "const" should mean. We ended up discussing - https://google.github.io/styleguide/cppguide.html#Use_of_const Which indicates that methods should only be const if they dont affect the "logical" state of the object. |
@seishun based on the discussion today we think only a very small subset of those which were being changed should be const. For example |
Consider this example: Napi::Object CreateObject(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
Napi::Object obj1 = Napi::Object::New(env);
Napi::Object obj2 = obj1; // two independent objects
obj1.Set(Napi::String::New(env, "msg"), info[0].ToString());
return obj2;
} If the |
@seishun I don't think I follow your point. In the interpretation we've made const tells you that calling the method won't change the logical state of the object when you call it. Not that the state can't be changed in some other way? |
My point is that changing the state of one object doesn't normally change the state of another object. See also #874 (comment). Another example: const Napi::Object obj1 = Napi::Object::New(env);
Napi::Object obj2 = obj1;
obj2.Set(Napi::String::New(env, "msg"), info[0].ToString()); I don't know how exactly you define "logical state" but if it's something that can change in a const object then it's not a very useful term. |
We were not talking about changing some unrelated object. For example, If the C++ object contains an internal reference to a handle to JavaScript object. Calling a method on the C++ object may not change any of the content in the C++ object but instead changes the content of the JavaScript object that the handle points to. That in turn may result in a different answer when you call another method on C++ object if it gets the result from the related JavaScript object. In that case we were considering the JavaScript object part of the "logical state". |
I'm saying that the content of a JavaScript object can't be considered part of the "logical state" of any specific C++ object by any useful definition if that content can be shared by multiple C++ objects and can change in a const object. |
I'm finding the const-ness definition is tricky in node-addon-api. All classes in node-addon-api are wappers on opaque pointers. They don't contain any meaningful state themselves. However, they do have a logical state that they are pointed to. So on the node-addon-api, I'm finding the const-ness is a guarantee that we are sure the operations don't have side effects either on the wrapper class itself and their underlying logical state, i.e. JavaScript values and operations. As such, I'd find any operation that would cause side effects yet didn't change the wrapper class in node-addon-api to be const is not intuitive and highly misleading. Although For the example described at #874 (comment), I'd find it's a problem on how we define the semantics on assign-and-copy of a node-addon-api wrapper class: const Napi::ObjectReference obj1 = Napi::ObjectReference::New(Napi::Object::New(info.Env()));
Napi::ObjectReference obj2 = obj1; // assign is disallowed. |
In the standard C++ library smart pointers have the The only way to prohibit calling non-const methods is to wrap const type inside of the smart pointers: In the Node-API, the wrappers around |
Please note in node-addon-api the methods named with "set" and "get" are not semantically identical to smart pointers' get and set. They're "set" and "get" for the JavaScript values. The wrapper class in node-addon-api is concrete JavaScript type, like |
What wrong idea does it give? |
In JavaScript any object variable that is declared as In C++
If we do add the the
If we do not add
While I understand the desire to use the |
I'm very appreciative of the explanation @vmoroz gave. I gave this another thought and find that the actual reason I don't find it correct is that the original idea of the change is allowing calling into non-const JavaScript engine methods from const c++ functions. The reason we can call into these node-api c-functions from const c++ functions is that they are c interfaces and these c interfaces can not have c++ const marks and c++ simply ignores the reality these c-functions implementation is not const, even if they are implemented in c++ and calling non-const c++ functions, which definitely breaks the assumptions that const c++ functions can only call into const functions. That is: (const) user functions -> (const) node-addon-api c++ wrappers -> node-api c interface (the barrier that invalidates the constness check) -> (non-const) node-api c++ implementations -> (non-const) Javascript c++ implementations. So when considering the fact that const c++ functions can only call into const c++ functions, this behavior already breaks the assumption. |
We discussed in the meeting again today and @vmoroz is going to find add the text from the spec in terms of what adding const to a method promises. |
I did some research and asked our Visual C++ discussion group about this issue. 3.37 [defns.observer] 9.5.1 Function definitions - In general [dcl.fct.def.general]/p5 7.5.2 This [expr.prim.this]/p3 9.2.9.2 The cv-qualifiers [dcl.type.cv]/p4 The standard does not say about any possible optimizations for The example of a program where using struct S1 {
void f() const { i = 0; }
mutable int i;
};
struct S2 {
void f() const { const_cast<S2*>(this)->i = 0; }
int i;
};
const S1 s1{};
const S2 s2{};
int main()
{
s1.f(); // ok
s2.f(); // crash
} Visual C++ compiler puts In our case we do not need to use any // C-based APIs
struct S1 { int i; };
int get_property(S1* s) { return s->i; }
void set_property(S1* s, int value) { s->i = value; }
// C++ wrapper for the C-based APIs
struct CppValue {
int GetProperty() const { return get_property(s); }
void SetProperty(int value) const { set_property(s, value); }
S1* s;
};
int main()
{
S1 s1;
const CppValue c{ &s1 };
c.SetProperty(5);
printf("%d\n", c.GetProperty());
} |
This example shows that function #include <cstdio>
// Methods are not const and can modify the class instance state
struct NonConstValue {
int GetProperty() { return prop; }
void SetProperty(int value) { prop = value; }
int prop;
};
// All methods are const and do not modify this class instance state
struct ConstValue {
int GetProperty() const { return val->GetProperty(); }
void SetProperty(int value) const { val->SetProperty(value); }
NonConstValue* val;
};
int main()
{
NonConstValue v1;
const ConstValue c1{ &v1 };
c1.SetProperty(5);
printf("%d\n", c1.GetProperty());
} We can call non-const methods on the |
Thanks for explaining with the examples! I'd believe I don't have any objections to this PR with the great illustration @vmoroz |
I think that example clarifies 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.
LGTM after rebase.
@seishun we talked through this today in the Node-API team meeting. Thanks for being so patient. Everybody seems comfortable with adding const now. Could you rebase and see if some of the new methods should also have const added and we will work to land it soon after. |
There was |
@seishun we discussed in the meeting and agreed those 2 could have const added as well. |
Just in case, those two already have const added. |
@seishun I'm wondering about the removal of the const iterator as that might affect existing code? |
Only if the code names the type explicitly, otherwise I don't think so. |
@seishun we'll need to discuss some more since, it could break some existing code. Is the removal necessary or could we get the rest of the changes landed first ? |
It's not necessary, the const iterator just doesn't make sense anymore. Feel free to land without that commit. |
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 minus the commit to remove the const operator. Will remove that when I land.
PR-URL: #874 Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: Michael Dawson <[email protected]
This commit breaks the following code: void foo (const CallbackInfo& info) {
const Object obj = info[0].As<Object>();
obj["xxx"].As<Boolean>();
} because the following methods were deleted in this commit /// Gets or sets an indexed property or array element.
PropertyLValue<Value> operator[](Value index /// Property / element index
) const;
/// Gets a named property.
MaybeOrValue<Value> operator[](
const char* utf8name ///< UTF-8 encoded null-terminated property name
) const;
/// Gets a named property.
MaybeOrValue<Value> operator[](
const std::string& utf8name ///< UTF-8 encoded property name
) const;
/// Gets an indexed property or array element.
MaybeOrValue<Value> operator[](uint32_t index ///< Property / element index
) const; |
@ShenHongFei could you open a new issue for this. Have you tried adding those 4 methods back or does that not work due to other changes? |
@ShenHongFei created new issue here - #1158. You can follow discussion/questions there. |
Fixes: nodejs#1158 Revert part of nodejs#874 to avoid breaking existing code. Signed-off-by: Michael Dawson <[email protected]>
Fixes: #1158 Revert part of #874 to avoid breaking existing code. PR-URL: #1159 (review) Reviewed-By: Kevin Eady <[email protected]> Signed-off-by: Michael Dawson <[email protected]>
PR-URL: nodejs/node-addon-api#874 Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: Michael Dawson <[email protected]
Fixes: nodejs/node-addon-api#1158 Revert part of nodejs/node-addon-api#874 to avoid breaking existing code. PR-URL: nodejs/node-addon-api#1159 (review) Reviewed-By: Kevin Eady <[email protected]> Signed-off-by: Michael Dawson <[email protected]>
PR-URL: nodejs/node-addon-api#874 Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: Michael Dawson <[email protected]
Fixes: nodejs/node-addon-api#1158 Revert part of nodejs/node-addon-api#874 to avoid breaking existing code. PR-URL: nodejs/node-addon-api#1159 (review) Reviewed-By: Kevin Eady <[email protected]> Signed-off-by: Michael Dawson <[email protected]>
PR-URL: nodejs/node-addon-api#874 Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: Michael Dawson <[email protected]
Fixes: nodejs/node-addon-api#1158 Revert part of nodejs/node-addon-api#874 to avoid breaking existing code. PR-URL: nodejs/node-addon-api#1159 (review) Reviewed-By: Kevin Eady <[email protected]> Signed-off-by: Michael Dawson <[email protected]>
PR-URL: nodejs/node-addon-api#874 Reviewed-By: Gabriel Schulhof <[email protected]> Reviewed-By: Michael Dawson <[email protected]
Fixes: nodejs/node-addon-api#1158 Revert part of nodejs/node-addon-api#874 to avoid breaking existing code. PR-URL: nodejs/node-addon-api#1159 (review) Reviewed-By: Kevin Eady <[email protected]> Signed-off-by: Michael Dawson <[email protected]>
My two cents on this topic and why I think this was incorrect. Using const correctness is about side effects and the mutability of an object's visible state. If, for example, invoking the 'Set' method alters the result of invoking the 'Get' method, then 'Set' must not be qualified with const. It appears that invoking "Get" does not in any way alter what happens when querying the object state in subsequent calls. Therefore, it has no visible side effects and should therefore be qualified as const. Use const is contagious and it must be used correctly to not cause semantic problems and lose its purpose. If everything is qualified as const, it is the same or worse than leaving everything without the qualification. At least by leaving everything unqualified, whoever interacts with the object should be forced to mark it as mutable explicitly, and never pass it as const argument (useless), to interact in a context where care must be taken with side effects. This way it's clear that it’s forced to accept that maybe interacting with this will cause side effects and not hiding this fact. |
Fixes #870.
Some const methods weren't documented as const, so I think it makes sense to separate this into two commits.
Are there any other methods that should fall under this change?