gchen
gchen

Reputation: 116

Share member variables with member object for API backwards compatibility during refactor

The Question

This question may seem strange. I want to have a member variable sharing the same address space as one of the member variables of another member variable. For example,

struct Bar { int a; }
struct Foo {
  Bar bar;
  union {  // make a2 share the same address space as bar.a
    int a2;
    int bar.a;  // invalid syntax
  }
}

int main() {
  Foo foo;
  foo.bar.a = 5;
  cout << foo.a2 << endl;  // 5
}

where I want foo.a2 to share the same address space as foo.bar.a. I'll also explain why I want to do this because there's probably a better approach that I haven't thought of.

Why / Background / Motivation

I'm refactoring a library and need to maintain API backwards compatibility (not ABI, just the public-ly facing variables, functions, classes, etc names/syntaxes need to be backwards compatible). There's a chunk of code/parameters that should really be moved into its own class, but I'm having real difficulty figuring out how to do so without breaking compatibility. For example, consider this watered down snippet:

struct NonlinearOptimizerParams {
  // these variables make sense as nonlinear optimization parameters
  int maxIterations, errorTol, method;
  // but these variables should be in a separate parameter struct
  int linearSolverType, linearSolverOrdering, linearSolverTolerance;
};

and client code:

int main() {
  NonlinearOptimizerParams params;
  int &lst1 = params.linearSolverTolerance;
  int *lst2 = &params.linearSolverTolerance;

  params.linearSolverTolerance = 2;
  cout << params.linearSolverTolerance << endl;  // 2
  lst1 = 3;
  cout << params.linearSolverTolerance << endl;  // 3
  *lst2 = 4;
  cout << params.linearSolverTolerance << endl;  // 4
}

I would like to split NonlinearOptimizerParams into 2 structs (the code should definitely be split this way based on the rest of the code base - I politely request that you trust me and refrain from objecting to my end goal):

struct LinearOptimizerParams {
    int linearSolverType, linearSolverOrdering, linearSolverTolerance;
};

struct NonlinearOptimizerParams {
    int maxIterations, errorTol, method;
    LinearOptimizerParams loParams;
};

where NonlinearOptimizerParams holds a member variable of type LinearOptimizerParams. But this would break backwards compatibility, ie params.linearSolverTolerance would need to change to params.loParams.linearSolverTolerance in client code.

So I want to keep deprecated variables in NonlinearOptimizerParams that "automatically synchronize" with the variables inside NonlinearOptimizerParams.loParams. i.e. I want params.linearSolverTolerance to be an "alias" for params.loParams.linearSolverTolerance. For example,

struct LinearOptimizerParams {
  int linearSolverType, linearSolverOrdering, linearSolverTolerance;
};

struct NonlinearOptimizerParams {
  int maxIterations, errorTol, method;
  LinearOptimizerParams loParams;
  int linearSolverType = loParams.linearSolverType; // @deprecated
  int linearSolverOrdering = loParams.linearSolverOrdering; // @deprecated
  int linearSolverTolerance = loParams.linearSolverTolerance; // @deprecated
};

except where the equals-signs represent a "symlink" kind of thing (aka share the same address space).

Is that even possible?

Preliminary attempts

Inheritance

One way I thought of was to let NonlinearOptimizerParams inherit from LinearOptimizerParams so that nonlinear would have access to all linear's member variables like so:

struct LinearOptimizerParams {
  int linearSolverType, linearSolverOrdering, linearSolverTolerance;
};

struct NonlinearOptimizerParams : public LinearOptimizerParams {
  int maxIterations, errorTol, method;
};

But the problem is that semantically this doesn't really make a ton of sense because a NonlinearOptimizer is not a type of LinearOptimizer. Similarly, a NonlinearOptimizerParams isn't a type of LinearOptimizerParams. Rather, a NonlinearOptimizerParams contains a LinearOptimizerParams. I worry that ignoring the semantics will cause maintainability issues down the road.

Union

Another, hacky suspected-undefined-behavior way I thought of is something like this:

struct LinearOptimizerParams {
  int linearSolverType, linearSolverOrdering, linearSolverTolerance;
};

struct NonlinearOptimizerParams {
  int maxIterations, errorTol, method;
  union {
      LinearOptimizerParams loParams;
      int linearSolverType, linearSolverOrdering, linearSolverTolerance;
  };
};

But this seems like a nightmare to make sure the order of variables stays consistent across machines and over time - not really acceptable code.

References as member variables

I also tried something like this where LinearOptimizerParams uses references as member variables:


struct LinearOptimizerParams {
  int &linearSolverType, &linearSolverOrdering, &linearSolverTolerance;
};

struct NonlinearOptimizerParams : LinearOptimizerParams {
  NonlinearOptimizerParams() {
    loParams.linearSolverType = linearSolverType;
    ...
  }
  int maxIterations, errorTol, method;
  int linearSolverType, linearSolverOrdering, linearSolverTolerance;
  LinearOptimizerParams loParams;
};

But now initializing LinearOptimizerParams becomes difficult and is incompatible with the rest of the API (I need a default constructor).

I know this is a bit of a strange question, but I hope someone has some ideas!

Upvotes: 0

Views: 93

Answers (1)

sklott
sklott

Reputation: 2849

I think the only sensible way is with references. But you did it backwards, i.e. try it this way:

struct LinearOptimizerParams {
  int linearSolverType, linearSolverOrdering, linearSolverTolerance;
};

struct NonlinearOptimizerParams : LinearOptimizerParams {
  int maxIterations, errorTol, method;
  LinearOptimizerParams loParams;
  int& linearSolverType {loParams.linearSolverType};
  int& linearSolverOrdering {loParams.linearSolverOrdering};
  int& linearSolverTolerance {loParams.linearSolverTolerance};
};

You even can do macros to define these parameters, so that you can shorten declaration/initialization.

The only issue with this approach is that you need to either delete or define copy/move constructors. Because default/implicit implementations ignore member initialization.

Upvotes: 1

Related Questions