'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