sol2 icon indicating copy to clipboard operation
sol2 copied to clipboard

Documentation about sol::function parameter passing misleading

Open Spacechild1 opened this issue 9 months ago • 0 comments

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";
}

Spacechild1 avatar Feb 11 '25 23:02 Spacechild1