Reputation: 2783
I've got several structures in my code which all have
std::pair
or cv::Point2d
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
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 c++17 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:
X
from structured<X>
.auto members(X const& x)
that returns a tuple of pointer-to-members you care about.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 c++14 just implement your own notstd::apply
.
In c++11 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
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
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