mtszkw
mtszkw

Reputation: 2783

Allow implicit casting for types with the same underlying structure

I've got several structures in my code which all have

Example:

struct CartesianCoordinates {
    std::pair<double, double> xy;

    void x(double val) { xy.first = val; }
    void y(double val) { xy.second = val; }
    double x() const { return xy.first; }
    double y() const { return xy.second; }
};

struct GeographicCoordinates {
    std::pair<double, double> xy;

    void longtitude(double val) { xy.first = val; }
    void lattitude(double val) { xy.second = val; }
    double longtitude() const { return xy.first; }
    double lattitude() const { return xy.second; }
};

Now I want to be able to convert between those types implicitly, like this:

CartesianCoordinates returnCartesian()
{
    CartesianCoordinates c;
    c.x(5);
    c.y(-13);
    return c;
}

void getGeographicCoordinates(const GeographicCoordinates& c) {
    std::cout << c.longtitude() << '\n';
}

int main()
{
    getGeographicCoordinates(returnCartesian());
}

Is there any way to achieve this without

In fact reinterpret_cast/pointers should work, but do any modern C++ mechanism exist that I am not aware of and that could help me to solve it with least structures implementation overhead, just using the fact that all structures have the same structure inside?

Upvotes: 2

Views: 289

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

No, not without lots of per-structure boilerplate.

In fact reinterpret_cast/pointers should work

Not actually true. You can reinterpret from struct to pair using the fact that the pair is the first element in the standard layout structure, but you can only reinterpret from pair to struct if that structure type is actually there.

If an object of the structure type isn't there, the reinterpret from pair to struct is undefined behavior.

Here is an attempt to do this in with minimal boilerplate while remaining generic. We do inherit from an empty CRTP empty parent type to inject some friends and conversion operators. Also, we have to add some code that exposes the members of each to the CRTP parent.

template<class A, class B>
struct tie_same;

template<class A, class B>
using tie_check = typename tie_same<A,B>::type;

template<class D>
struct structured {
  friend auto as_tie( structured<D>& s ) {
    auto& self = static_cast<D&>(s);
    auto members = get_members(self);
    return std::apply( [&self](auto...pm) {
      return std::tie( (self.*pm)... );
    }, members );
  }
  friend auto as_tie( structured<D> const& s ) {
    auto& self = static_cast<D const&>(s);
    auto members = get_members(self);
    return std::apply( [&self](auto...pm) {
      return std::tie( (self.*pm)... );
    }, members );
  }

  template<class T,
    std::enable_if_t<
      tie_check<T,D>{},
      bool
    > = true
  >
  operator T() const& {
    T tmp;
    as_tie(tmp) = as_tie( *this );
    return tmp;
  }
};

template<class A, class B>
struct tie_compatible {
  template<class X>
  using tie_type = decltype( as_tie( std::declval<X>() ) );
  using type = std::is_same< tie_type<A&>, tie_type<B&> >;
};

a bit tricky but does the job.

To support this system in a type X, you need to:

  1. inherit X from structured<X>.
  2. Implement auto members(X const& x) that returns a tuple of pointer-to-members you care about.
  3. Have a default constructor for your target type, and be ok with construction-through-assignment.

Here are your types augmented with this feature:

struct CartesianCoordinates: structured<CartesianCoordinates> {
  std::pair<double, double> xy;

  friend auto get_members( CartesianCoordinates const& self ) {
    return std::make_tuple( &CartesianCoordinates::xy );
  }

  void x(double val) { xy.first = val; }
  void y(double val) { xy.second = val; }
  double x() const { return xy.first; }
  double y() const { return xy.second; }
};

struct GeographicCoordinates: structured<GeographicCoordinates> {
  std::pair<double, double> xy;

  friend auto get_members( GeographicCoordinates const& self ) {
    return std::make_tuple( &GeographicCoordinates::xy );
  }

  void longtitude(double val) { xy.first = val; }
  void lattitude(double val) { xy.second = val; }
  double longtitude() const { return xy.first; }
  double lattitude() const { return xy.second; }
};

In just implement your own notstd::apply.

In it gets annoying due to the lack of return type deduction.

#define RETURNS(...) \
   noexecpt(noexcept(__VA_ARGS__)) \
   -> decltype(__VA_ARGS__) \
   { return __VA_ARGS__; }

RETURNS can patch over that pain a bit.

auto foo()
  RETURNS( 1+2 )

replacing

auto foo() {
  return 1+2;
}

sometimes works. (There are important differences still, but what can you do)

Upvotes: 0

rubenvb
rubenvb

Reputation: 76519

This can be solved by defining an access interface to the members.

I would suggest you reuse the get idiom and define templated (on an index) get free functions for all the structs you want this behaviour to work on.

Then you must define the functions that should work on all these types in a similar manner as templates, and access the members through the get interface.

Upvotes: 0

NathanOliver
NathanOliver

Reputation: 180510

Well, as long as you don't want to transform the data in the data member, and as long as the member is always named the same thing, you can use a template conversion operator. Then you use SFINAE to constrain the template for only types that have the named member that is the same type as the named member of the class. Adding

template <typename T, std::enable_if_t<std::is_same_v<std::pair<double, double>, decltype(std::declval<T&>().xy)>>* = nullptr>
operator T() { return T{xy}; }

To both classes allows them to be converted to one another and allows

int main()
{
    getGeographicCoordinates(returnCartesian());
}

to run (live example)


If the classes will not use the same member variable name then you could have them all surface a typedef that is named the same that is the type of the data member and use that. That would look like

template <typename T, std::enable_if_t<std::is_same_v<my_variable_typedef, typename T::my_variable_typedef>>* = nullptr>
operator T() { return T{xy}; }

Upvotes: 4

Related Questions