'Friend function not found by inner class in Type Erasure setup C++
I watched Klaus Iglberger give a great talk at CppCon 2021 about Type Erasure. I went to set up the pattern myself based on his example. His External Polymorphic piece dispatched to a friend function of an outer class. That piece is not compiling for me.
I've tried shuffling around the order of definition and pulling the implementation of the friend function outside the class. I didn't set up an implementation file yet because I wasn't very convinced that would be a solution.
I'm using GCC 9.3 and Clang 10. Errors look the same. Clang10:
type_erasure_example.cpp:57:22: error: too many arguments to function call, expected 0, have 1 serialize( object );
type_erasure_example.cpp:61:28: error: too many arguments to function call, expected 2, have 3 draw( object, x, y );
GCC9.3:
type_erasure_example.cpp:57:11: error: no matching function for call to ‘Shape::ShapeModel<Square>::serialize(const Square&) const’ 57 | serialize( object );
type_erasure_example.cpp:61:11: error: no matching function for call to ‘Shape::ShapeModel<Square>::draw(const Square&, int&, int&) const’ 61 | draw( object, x, y );
#include <iostream>
#include <memory>
#include <vector>
class Circle
{
public:
explicit Circle( double rad )
: radius{ rad }
// ... Remaining data members
{}
double getRadius() const noexcept { return radius; }
// ... getCenter(), getRotation(), ...
private:
double radius;
// ... Remaining data members
};
class Square
{
public:
explicit Square( double s )
: side{ s }
// ... Remaining data members
{}
double getSide() const noexcept { return side; }
// ... getCenter(), getRotation(), ...
private:
double side;
// ... Remaining data members
};
class Shape
{
private:
struct ShapeConcept
{
virtual ~ShapeConcept() {}
virtual void serialize() const = 0;
virtual void draw(int x, int y) const = 0;
virtual std::unique_ptr<ShapeConcept> clone() const = 0;
};
template< typename T >
struct ShapeModel : ShapeConcept
{
ShapeModel( T&& value )
: object{ std::forward<T>(value) }
{}
std::unique_ptr<ShapeConcept> clone() const override
{
return std::make_unique<ShapeModel>(*this);
}
void serialize() const override
{
serialize( object ); // Error
}
void draw( int x, int y ) const override
{
draw( object, x, y ); // Error
}
T object;
};
friend void serialize( Shape const& shape )
{
shape.pimpl->serialize();
}
friend void draw( Shape const& shape, int x, int y )
{
shape.pimpl->draw( x, y );
}
std::unique_ptr<ShapeConcept> pimpl;
public:
template< typename T >
Shape( T&& x )
: pimpl{ new ShapeModel<T>( std::forward<T>(x) ) }
{}
// Special member functions
Shape( Shape const& s );
Shape( Shape&& s );
Shape& operator=( Shape const& s );
Shape& operator=( Shape&& s );
};
void serialize( Circle const& circle ) {
std::cout << "Serializing a circle with radius "
<< circle.getRadius() << std::endl;
}
void draw( Circle const& circle, int x, int y ) {
std::cout << "Drawing a circle with radius "
<< circle.getRadius() << " Coordinates ("
<< x << ", " << y << ")" << std::endl;
}
void serialize( Square const& square ) {
std::cout << "Serializing a square with side "
<< square.getSide() << std::endl;
}
void draw( Square const& square, int x, int y ) {
std::cout << "Drawing a square with side "
<< square.getSide() << " Coordinates ("
<< x << ", " << y << ")" << std::endl;
}
void drawAllShapes( std::vector<Shape> const& shapes )
{
for( auto const& shape : shapes )
{
draw( shape , 0.0, 0.0 );
}
}
int main()
{
using Shapes = std::vector<Shape>;
// Creating some shapes
Shapes shapes;
shapes.emplace_back( Circle{ 2.0 } );
shapes.emplace_back( Square{ 1.5 } );
shapes.emplace_back( Circle{ 4.2 } );
// Drawing all shapes
drawAllShapes( shapes );
}
Solution 1:[1]
Someone posted in the comments and it set me on the right track. I didn't understand the setup well enough. The friend classes were being handled right. It was the free functions that needed re-arranging. An implementation file would've helped afterall, and we can do it header-only:
#include <iostream>
#include <memory>
#include <vector>
class Circle
{
public:
explicit Circle( double rad )
: radius{ rad }
// ... Remaining data members
{}
double getRadius() const noexcept { return radius; }
// ... getCenter(), getRotation(), ...
private:
double radius;
// ... Remaining data members
};
class Square
{
public:
explicit Square( double s )
: side{ s }
// ... Remaining data members
{}
double getSide() const noexcept { return side; }
// ... getCenter(), getRotation(), ...
private:
double side;
// ... Remaining data members
};
void serialize( Circle const& circle ) {
std::cout << "Serializing a circle with radius "
<< circle.getRadius() << std::endl;
}
void draw( Circle const& circle, int x, int y ) {
std::cout << "Drawing a circle with radius "
<< circle.getRadius() << " Coordinates ("
<< x << ", " << y << ")" << std::endl;
}
void serialize( Square const& square ) {
std::cout << "Serializing a square with side "
<< square.getSide() << std::endl;
}
void draw( Square const& square, int x, int y ) {
std::cout << "Drawing a square with side "
<< square.getSide() << " Coordinates ("
<< x << ", " << y << ")" << std::endl;
}
class Shape
{
private:
struct ShapeConcept
{
virtual ~ShapeConcept() {}
virtual void serialize() const = 0;
virtual void draw(int x, int y) const = 0;
virtual std::unique_ptr<ShapeConcept> clone() const = 0;
};
template< typename T >
struct ShapeModel : ShapeConcept
{
ShapeModel( T&& value )
: object{ std::forward<T>(value) }
{}
std::unique_ptr<ShapeConcept> clone() const override
{
return std::make_unique<ShapeModel>(*this);
}
void serialize() const override
{
::serialize( object );
}
void draw( int x, int y ) const override
{
::draw( object, x, y );
}
T object;
};
friend void serialize( Shape const& shape )
{
shape.pimpl->serialize();
}
friend void draw( Shape const& shape, int x, int y )
{
shape.pimpl->draw( x, y );
}
std::unique_ptr<ShapeConcept> pimpl;
public:
template< typename T >
Shape( T&& x )
: pimpl{ new ShapeModel<T>( std::forward<T>(x) ) }
{}
// Special member functions
Shape( Shape const& s ) = default;
Shape( Shape&& s ) = default;
Shape& operator=( Shape const& s ) = default;
Shape& operator=( Shape&& s ) = default;
};
void drawAllShapes( std::vector<Shape> const& shapes )
{
for( auto const& shape : shapes )
{
draw( shape , 0.0, 0.0 );
}
}
int main()
{
using Shapes = std::vector<Shape>;
// Creating some shapes
Shapes shapes;
shapes.emplace_back( Circle{ 2.0 } );
shapes.emplace_back( Square{ 1.5 } );
shapes.emplace_back( Circle{ 4.2 } );
// Drawing all shapes
drawAllShapes( shapes );
}
Solution 2:[2]
Thanks for your code, but I think you forgot to properly define the Copy Constructor and Assignment Operator of Shape using clone() so you can treat a Shape as a value as Klaus Iglberger suggested. I've added this together with a scale() function to test this in main().
#include <iostream>
#include <memory>
#include <vector>
class Circle
{
public:
explicit Circle( double rad )
: radius{ rad }
// ... Remaining data members
{}
double getRadius() const noexcept { return radius; }
void setRadius(double rad) noexcept { radius=rad; }
// ... getCenter(), getRotation(), ...
private:
double radius;
// ... Remaining data members
};
class Square
{
public:
explicit Square( double s )
: side{ s }
// ... Remaining data members
{}
double getSide() const noexcept { return side; }
void setSide(double s) noexcept { side=s; }
// ... getCenter(), getRotation(), ...
private:
double side;
// ... Remaining data members
};
void serialize( Circle const& circle ) {
std::cout << "Serializing a circle with radius "
<< circle.getRadius() << std::endl;
}
void draw( Circle const& circle, int x, int y ) {
std::cout << "Drawing a circle with radius "
<< circle.getRadius() << " Coordinates ("
<< x << ", " << y << ")" << std::endl;
}
void scale( Circle& circle, double scale_factor ) {
circle.setRadius( circle.getRadius() * scale_factor );
}
void serialize( Square const& square ) {
std::cout << "Serializing a square with side "
<< square.getSide() << std::endl;
}
void draw( Square const& square, int x, int y ) {
std::cout << "Drawing a square with side "
<< square.getSide() << " Coordinates ("
<< x << ", " << y << ")" << std::endl;
}
void scale( Square& square, double scale_factor ) {
square.setSide( square.getSide() * scale_factor );
}
class Shape
{
private:
struct ShapeConcept
{
virtual ~ShapeConcept() {}
virtual void serialize() const = 0;
virtual void draw(int x, int y) const = 0;
virtual std::unique_ptr<ShapeConcept> clone() const = 0;
virtual void scale(double scale_factor)=0;
};
template< typename T >
struct ShapeModel : ShapeConcept
{
ShapeModel( T&& value )
: object{ std::forward<T>(value) }
{}
std::unique_ptr<ShapeConcept> clone() const override
{
return std::make_unique<ShapeModel>(*this);
}
void serialize() const override
{
::serialize( object );
}
void draw( int x, int y ) const override
{
::draw( object, x, y );
}
void scale(double scale_factor) override
{
::scale(object,scale_factor);
}
T object;
};
friend void serialize( Shape const& shape )
{
shape.pimpl->serialize();
}
friend void draw( Shape const& shape, int x, int y )
{
shape.pimpl->draw( x, y );
}
friend void scale( Shape& shape, double scale_factor)
{
shape.pimpl->scale(scale_factor);
}
std::unique_ptr<ShapeConcept> pimpl;
public:
template< typename T >
Shape( T&& x )
: pimpl{ new ShapeModel<T>( std::forward<T>(x) ) }
{}
// Special member functions
Shape( Shape const& s ) : pimpl{ s.pimpl->clone() } {}
Shape& operator=( Shape const& s ) { this->pimpl = s.pimpl->clone(); return *this; }
Shape( Shape&& s ) = default;
Shape& operator=( Shape&& s ) = default;
};
void drawAllShapes( std::vector<Shape> const& shapes )
{
for( auto const& shape : shapes )
{
draw( shape , 0.0, 0.0 );
}
}
int main()
{
using Shapes = std::vector<Shape>;
// Creating some shapes
Shapes shapes;
shapes.emplace_back( Circle{ 2.0 } );
shapes.emplace_back( Square{ 1.5 } );
shapes.emplace_back( Circle{ 4.2 } );
// Drawing all shapes
drawAllShapes( shapes );
// Copying and scaling all shapes
auto shapes_scaled = shapes; // copy shapes, here clone() is used
for ( auto& shape : shapes_scaled )
scale( shape, 10 );
drawAllShapes( shapes_scaled ); // scaled by factor 10
drawAllShapes( shapes ); // left untouched
}
Solution 3:[3]
To complete the example with the kind help of Klaus I fixed the copy/move/assign issues by adding a trait and constrain on the templated constructor based on @bterwijn post
...
class Shape;
template< typename T >
struct IsShape : public std::is_same< Shape, std::decay_t<T> >
{};
...
template<typename T, typename = std::enable_if_t<!IsShape<T>::value>>
Shape( T&& x ) : pimpl{ new ShapeModel<T>( std::forward<T>(x) ) } { }
Here is a complete working example (Tested on GCC, CLANG and MSVC) https://wandbox.org/permlink/BvOngSRPXtEnFPMR
Note there are no checks on empty shapes (i.e. pimpl == nullptr)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | user2183336 |
| Solution 2 | |
| Solution 3 | Nir |
