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

Is there a way to reflect private fields? #66

Open
bboysnick5 opened this issue Feb 13, 2022 · 5 comments
Open

Is there a way to reflect private fields? #66

bboysnick5 opened this issue Feb 13, 2022 · 5 comments

Comments

@bboysnick5
Copy link

A bit of newbie question I guess. With the current status of C++, are there any ways we can reflect on private member fields or methods?

My main use case is de-serialization and populate class private fields from a parser. What's the best approach if reflection is not possible? I'm willing to friend or expose to a particular builder but not public the whole data section. Thanks.

@RalphSteinhagen
Copy link
Contributor

@bboysnick5 yes, there are technically several possibilities to access private fields, most of them rely on C-style pointer-arithmetics (pointer-casting, storing private field offsets within the class, ...), for example using reinterpret_cast (compiler explorer):

class Weak {
private:
    std::string name;

public:
    void setName(const std::string& name) { this->name = name; }
    std::string getName()const { return this->name; }
};

struct Hacker { // being a privately known shaddow-class/copy of 'Weak'
    std::string name;
};

int main(int argc, char** argv) {
    Weak w;
    w.setName("Jon");
    std::cout << w.getName() << std::endl;
    Hacker *hackit = reinterpret_cast<Hacker *>(&w);
    hackit->name = "Jack";
    std::cout << w.getName() << std::endl;
}

However, this generally is not considered good practice especially you hinted at encapsulation as a requirement (i.e. "not public the whole data").

The most popular, other, and cleaner approaches for (de-)deserialisation w/o accessing fields are, for example, to

  • reflect on the available getter-/setter-methods, or
  • reflect and provide (optionally private) (de-)serialise method functions similar to how one implements operator<<, or
  • ... [add a dozen more options here]

Whatever your preference is, refl-cpp is versatile enough to support these and many other possible patterns.

It all depends a bit on your application, API design style, which/how many protocols you want to support, and other requirements...

@bboysnick5
Copy link
Author

bboysnick5 commented Feb 14, 2022

@RalphSteinhagen Thanks for the detailed answer. Appreciated.

For the hacking method, I'm not seeing a way to actually pull the full-fledged shadow struct declaration off of the header file, w/o indirections involving pointers like pimpl. If I can and possibly encapsulate that into a namespace provided that the padding and alignment are the same, I'm ok with this approach.

Setters are really tedious and something I don't want to touch...

My concrete problem has the program flow as follow,

  1. An input text file and each line representing an instance of the deserialize-to class. (possibly to swap to byte stream or other protocols at a later time)
  2. split each line into tokens.
  3. every class data field can be mapped to one or multiple tokens. So i'm thinking reflect on each field with custom attribute being{The type of the class field to return to, the reduction operator, a number of parsing signatures to parse each token(i.e. return type, token format string, etc)}.
  4. Assume I can reflect on private member fields, a compile time refl::for_each would suffice.

Can you advise on how I should solve this?

Thanks again.

@RalphSteinhagen
Copy link
Contributor

I guess you'll have always some code duplication with the constraints you mentioned: either registering and synchronising the getter/setter, or a method (that e.g. takes your text file line) to the private field layout, or synchronising a shadow to the public class definition.

You could always put the shadow class (i.e. Hacker in the above example) in a sub-namespace, for example, `namespace my_name_space::detail { /.../ }' which syntactically should indicate to users of your library not to rely on the specific implementation there... in terms of (type) safety: it's then still up to you (effort wise) to synchronise the two struct copies (albeit the shadow one being rather simple) and the use would be always able to reverse-engineer and hack into your private member fields ... there's nothing really that can prevent that.

The choice depends a lot on how many classes and field-per-classes you have... hundreds of classes with few fields and/or few classes with hundreds of fields ... or something in between.

For what it's worth: using compile-time reflection is IMHO still a better choice than relying on IDL-type serialiser that require a code-generator to instantiate the classes.

You seem to hint at an object-oriented programming style... why not use (possibly immutable) plain-old data structs and a functional programming style? That makes things usually also easier in terms of concurrency and thread safety.

Do you have a link to your project?

@bboysnick5
Copy link
Author

bboysnick5 commented Feb 15, 2022

or a method (that e.g. takes your text file line) to the private field layout, or synchronising a shadow to the public class definition.

This is what I'm currently have, either a manual contructor-ish method or a friend builder class that manually invoke the corresponding parsing function and assign to the fields of the concrete class accordingly. But with checked the correctness of the parsing result using std::expected, it becomes really tedious to duplicate all the codes.

I have a few classes with each of them double digit data fields.

You seem to hint at an object-oriented programming style... why not use (possibly immutable) plain-old data structs and a functional programming style? That makes things usually also easier in terms of concurrency and thread safety.

This part I don't quite get it. Can you elaborate more?

I have posted the abstraction of my code in pseudo and sloppy style. Forgive my syntax. The create() method in ConcreteBuilder class is what I have for now and the place I want to refactor. compiler explorer link

namespace parser {

enum class ParsingPattern {
    kParseUint = 0,
    // ...
}

template <class ReturnType, FieldFormat format, ParsingPattern Pattern>
requires(Pattern == ParsingPattern::kParseUint) 
constexpr tl::expected<ReturnType, std::runtime_error> ParseField(std::string_view field_sv);

// other parsing pattern specialization
}


class Concrete {
public:
    // some methods

private: 
    int field_a;
    int64_t field_b

    friend class ConcreteBuilder;
};


class ConcreteBuilder {
    std::optional<Concrete> Create() {
        Concrete c;
        if (auto&& field_a_res = ParseField<decltype(field_a), {"%d"}, ParsingPattern::kParseInt>("42"); field_a_res.has_value() {
            c.field_a = field_a_res.value();
        } else {
            LOG(ERROR) << field_a_res;
            return std::nullopt;
        }  

        auto&& field_b_part1_res = ParseField<field_b_part1_type, {"%d"}, ParsingPattern::kParseInt>("42");
        auto&& field_b_part2_res = ParseField<field_b_part2_type, {"%d"}, ParsingPattern::kParseInt>("42");
        if (!field_b_part1_res.has_value()) {
            LOG(ERROR) << field_b_part1_res;
            return std::nullopt;
        } else if (!field_b_part2_res.has_value()) {
            LOG(ERROR) << field_b_part2_res;
            return std::nullopt;
        } else {
            c.field_b = field_b_reduction_op()(field_b_part1_res, field_b_part1_res);
        }

        // other fields

        return c;  
    }
};

@schaumb
Copy link

schaumb commented Jul 27, 2022

If you don't able to write to the struct, but you know the variable's name, constexpr time can be "get" private members/member functions by their pointer, see this repo

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

No branches or pull requests

3 participants