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

Add Support for Printing Generated Slice Types in C++ through operator<< #3324

Open
InsertCreativityHere opened this issue Jan 8, 2025 · 3 comments

Comments

@InsertCreativityHere
Copy link
Member

As discussed in https://github.com/orgs/zeroc-ice/discussions/2980, we should add support for printing generated Slice types.
We should add support for printing the following:

  • Builtin (most of these are already supported by virtue of being mapped to primitive types)
  • Class
  • Interface (really proxy)
  • Struct
  • Sequence
  • Dictionary
  • Enum

What should we do for exceptions?
Since exceptions are not types, they can't be held in any of the above Slice definitions.
Additionally, all the generated exceptions inherit support for operator<< from Ice::Exception:

ostream&
Ice::operator<<(ostream& out, const Ice::Exception& ex)
{
if (ex.ice_file() && ex.ice_line() > 0)
{
out << ex.ice_file() << ':' << ex.ice_line() << ' ';
}
ex.ice_print(out);
// We don't override ice_print when we have a custom _what message. And a custom what message typically does not
// repeat the Slice type ID.
if (ex._what)
{
out << ' ' << ex._what;
}
string stack = ex.ice_stackTrace();
if (!stack.empty())
{
out << "\nstack trace:\n" << stack;
}
return out;
}

and we have the cpp:ice_print metadata, allowing users to provide their own ice_print implementations.

@InsertCreativityHere
Copy link
Member Author

Proposed Implementation:

Classes

The printed representation of a class will look like:

CLASS_NAME@ADDRESS {FIELD1NAME:FIELD1VALUE, FIELD2NAME:FIELD2VALUE, ...}

For example, printing an instance of a class generated from this:

class Device
{
    int uid;
    string modelName;
}

could look like:

Device@0x7683478 {uid:12, modelName:foobar}

One interesting note about classes is that they could possibly be recursive.
We can handle this by keeping a stack (vector) of ValuePtr and pushing/popping classes to it as we deeply traverse a class tree for printing. If we encounter a cycle (ie. we hit a class that'si already in our stack), instead of printing it's fields we instead print:

Device@0x7683478 {skipping already-seen recursive value...}

To achieve this, we'll generate 2 additional methods for classes:

ice_print_impl(ostream& os, vector<ValuePtr> classStack) const override; // Handles the heavy lifting.

ostream& operator<<(ostream& os, const <SELF>& p)
{
    p->ice_print_impl(os, {});
    return os;
}

Proxies

Proxies should already be fine. All user-generated proxies inherit from ObjectPrx and we already have support for printing those:

ostream&
Ice::operator<<(ostream& os, const Ice::ObjectPrx& p)
{
    return os << p.ice_toString();
}

Structs

Structs would work in the exact same way as classes. Same representation, and we'd generate the same 2 functions.
The only difference is that we won't push/pop structs onto the classStack, since there's no risk of having a recursive struct.

Sequences & Dictionaries

Still thinking of what to do here.

Enum

For enums we would generate the enumerator name (if possible), and the raw underlying value otherwise.
For example, for this Slice definition:

enum Foo { Hello, There };

we would generate this implementation directly under the enum's generated definition:

ostream& operator<<(ostream& os, <SELF> value)
{
    switch (value)
    {
        case Foo::Hello:
            return os << "Foo::Hello";
        case Foo::There:
            return os << "Foo::There";
        default:
            return os << "Foo {" << static_cast<uint8_t>(value) >> "}";     // the compiler knows to generate `uint8_t` or `int32_t`.
    }
}

@bernardnormier
Copy link
Member

As I mentioned on the discussion, I would use the same formatting as the ToString synthesized for records in C#:
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/record#built-in-formatting-for-display

@InsertCreativityHere
Copy link
Member Author

Should we still make the slight adjustment of printing the memory address after the name?:

Device@0x56f23481 { uid = 12, modelName = Davolio }

Otherwise it will be impossible to tell when 2 classes are the same instance as opposed to copies,
or in the case of recursive classes, impossible to tell exactly which instance was recursive.

I know that we're basing it on C#, but other languages like Python do include memory addresses when printing non-primitive types like these.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants