sol2
sol2 copied to clipboard
Documentation about sol::function parameter passing misleading
Here's a thing that recently bit me. https://sol2.readthedocs.io/en/latest/functions.html#functions-and-argument-passing says the following about function argument passing:
All arguments are forwarded. Unlike get/set/operator[] on sol::state or sol::table, value semantics are not used here. It is forwarding reference semantics, which do not copy/move unless it is specifically done by the receiving functions / specifically done by the user.
But this is not entirely correct. If you pass an object to a sol::function by const-reference, the object is copied!
The following table compares the behavior of function calls with table setters:
| Type | function call | table setter |
|---|---|---|
| T& | reference | copy |
| const T& | copy | copy |
| (const) T* | reference | reference |
| T&& | move | move |
While the table setter is consistent in its treatment of reference arguments, function calls treat const T& differently from T&. Looking at the source code, this seems to be done on purpose, but I'm not really sure I understand the rationale. I would have expected both to be passed by reference! Anyway, the document should reflect the actual behavior.
Here's some code that demonstrates the behavior:
#include <sol/sol.hpp>
#include <iostream>
struct Foo {
float x = 0;
float y = 0;
Foo() {
std::cout << "Foo::Foo()\n";
}
Foo(float x_, float y_)
: x(x_), y(y_) {
std::cout << "Foo::Foo(float, float)\n";
}
~Foo() {
std::cout << "Foo::~Foo()\n";
}
Foo(const Foo& other)
: x(other.x), y(other.y) {
std::cout << "Foo::Foo(const Foo&)\n";
}
Foo(Foo&& other)
: x(other.x), y(other.y) {
std::cout << "Foo::Foo(Foo&&)\n";
}
Foo& operator=(const Foo& other) {
std::cout << "Foo::operator=(const Foo&)\n";
x = other.x;
y = other.y;
return *this;
}
Foo& operator=(Foo&& other) {
std::cout << "Foo::operator=(Foo&&)\n";
x = other.x;
y = other.y;
return *this;
}
};
int main(int argc, const char** argv)
{
sol::state state;
state.open_libraries();
state.new_usertype<Foo>(
"Foo",
"x", &Foo::x,
"y", &Foo::y
);
state.script(R"(
function takeFoo(foo)
print("passFoo:", foo.x, foo.y)
return foo
end
)");
{
std::cout << "a. function call:\n";
std::cout << "\n";
Foo foo(3, 4);
std::cout << "\n";
std::cout << "#1 non-const ref\n";
// 'foo' is passed by reference!
state["takeFoo"](foo);
std::cout << "\n";
std::cout << "#2 const ref\n";
// 'foo' is copied!!
state["takeFoo"](static_cast<const Foo&>(foo));
std::cout << "\n";
std::cout << "#3 pointer\n";
// 'foo' is passed by reference/pointer
state["takeFoo"](&foo);
std::cout << "\n";
std::cout << "#4 rvalue ref\n";
// 'foo' is moved
state["takeFoo"](std::move(foo));
std::cout << "\n";
}
std::cout << "---\n";
std::cout << "\n";
{
std::cout << "b. table setter:\n";
Foo foo(3, 4);
std::cout << "\n";
std::cout << "#1 non-const ref\n";
// 'foo' is copied
state["x"] = foo;
std::cout << "\n";
std::cout << "#2 const ref\n";
// 'foo' is copied
state["x"] = static_cast<const Foo&>(foo);
std::cout << "\n";
std::cout << "#3 pointer\n";
// 'foo' is passed by reference/pointer
state["x"] = static_cast<const Foo*>(&foo);
std::cout << "\n";
std::cout << "#4 rvalue ref\n";
// 'foo' is moved
state["x"] = std::move(foo);
std::cout << "\n";
}
std::cout << "---\n";
std::cout << "\n";
std::cout << "done!\n";
}