I’m trying to make a class to create a normal shape (equilateral shape) using OpenGL on Mac.
The given parameters are a center coordinate (double center[2]
), a radius from center to a vertex (double radius
), and a number of sides (int sides
):
#include "../vendors/GLFW/glfw3.h"
#include <cmath>
class EquilateralShape {
public:
int sides;
double center[2];
double radius;
double default_center[2] = {0.0, 0.0};
EquilateralShape(int sidesIn = 3, double centerIn[2] = default_center, double radiusIn = 1.0) {
sides = sidesIn;
center[0] = centerIn[0];
center[1] = centerIn[1];
radius = radiusIn;
}
// rendering the shape
void render() {
// calculating vertices of the polygon
double previous_vertex[2] = {center[0] + radius, center[1]};
for(int i = 1; i < sides + 1; i++) {
// determining if angle is greater than 90, 180, 270, or 360
double angle = (360*i)/sides;
bool x_neg = false;
bool y_neg = false;
if (angle > 270) {
angle = 360 - angle;
y_neg = true;
} else if (angle > 180) {
angle = -1 * (180-angle);
y_neg = true;
x_neg = true;
} else if (angle > 90) {
angle = 180 - angle;
x_neg = true;
}
// calculating vertices
double y_coord = radius * sin(angle);
double x_coord = radius - radius * cos(angle);
// updating coordinates to match unit circle group
if (x_neg == true) {
x_coord *= -1;
}
if (y_neg == true) {
y_coord *= -1;
}
// rendering triangle
glBegin(GL_TRIANGLES);
glVertex2d(x_coord, y_coord);
glVertex2d(previous_vertex[0], previous_vertex[1]);
glVertex2d(center[0], center[1]);
glEnd();
// updating previous vertex
previous_vertex[0] = x_coord;
previous_vertex[1] = y_coord;
}
}
};
…but when I compile, I’m told:
error: invalid use of non-static data member 'default_center'
This refers to the defaults parameter I passed for center.
Originally I tried not including a default parameter, but that also gave me an error:
error: missing default argument on parameter 'centerIn'
I also tried passing the default parameter as an array, in the form double centerIn[2] = {0.0, 0.0}
but the compiler told me it interpreted the comma between the array entries as a new argument.
3
Parameter default values are substituted in by the compiler only at the call site. They are not part of the parameter itself. So, you can’t use a non-static
class member as a parameter’s default value.
For example, this code:
void someFunction {
EquilateralShape shape;
...
}
is actually treated by the compiler as this code:
void someFunction {
EquilateralShape shape(3, ??.default_center, 1.0); // ERROR
...
}
See the problem? Whose default_center
member do we use? The shape
object doesn’t exist yet, so we can’t use its default_center
member.
If you make the class member be static
and const
then it will work the way you want, eg:
class EquilateralShape {
public:
static const double default_center[2];
EquilateralShape(int sidesIn = 3, const double centerIn[2] = default_center, double radiusIn = 1.0) {
...
}
};
// and then in some .cpp file:
const double EquilateralShape::default_center[2] = {0.0, 0.0};
Or, in C++17 and later, you can inline the initialization of the static
member:
class EquilateralShape {
public:
inline static const double default_center[2] = {0.0, 0.0};
// or:
static constexpr double default_center[2] = {0.0, 0.0};
EquilateralShape(int sidesIn = 3, const double centerIn[2] = default_center, double radiusIn = 1.0) {
...
}
};
And then this code:
void someFunction {
EquilateralShape shape;
...
}
is actually this code:
void someFunction {
EquilateralShape shape(3, EquilateralShape::default_center, 1.0); // OK
...
}
This is a follow up answer to Remy’s, showing the shortcomings of using (in particular) C-style arrays when you should be using proper types instead.
Your code can be reduced to the following minimal example, substituting Point
for your array of 2 doubles
:
struct Point { double x; double y; };
class Shape
{
public:
Shape (Point centre = { }) { /* ... */ }
};
int main ()
{
Shape my_shape;
}
See how simple and expressive that is? By using the type system properly your problem has magically disappeared, with a lot more benefits besides which you will discover along the way.
It’s always better to use std::array
than raw arrays. There are far less ways to get confused:
#include <array>
#include <cstdint>
struct EquilateralShape {
std::uint32_t sides;
std::array<double, 2> center;
double radius;
/*rest of declarations*/
};
then use constructor member initialzer list:
EquilateralShape(int sides_in = 3, std::array<double, 2> center_in = {}, double radius_in = 1.0):
sides{sides_in},
center{center_in},
radius{radius_in}
{/*Extra init*/};
If you intend to keep all data members publicly accessible, then it’s better to provide default values for members and rely on aggregate construction with designated initializations:
struct EquilateralShape {
std::uint32_t sides = 3;
std::array<double, 2> center = {};
double radius = 1.0;
/*No constructors*/
};
EquilateralShape shape1 {.radius{3.0}};
Raw arrays have very different semantics from ordinary types in C++. In some old C texts they are referred to as reference types; but since reference has a clear and very distinct definition in C++, those old C terms are no more used.
Raw array instances have following properties:
- They can’t be passed to functions by value. Instead they decay to pointers to first element, when passed to functions.
- They can’t be copy/move constructed
- They can’t be assigned to or from, as a whole object. Each element needs to be individually assigned.
The above properties makes using raw pointers difficult. But all of theses restrictions can be circumvented if the raw array is wrapped within an aggregate class, and that is exactly what std::array
does. On the unlikely event where compiler falls short of properly optimize std::array
, the data
and size
member functions provide the interface to switch back to C style code; but I seriously recommend against doing that.