How does this do what it does (C++ classes, use of colon)

For the first time I'm trying to get my feet seriously wet with C++ classes. Parts of the code below were just beg, steal and borrow :wink: It was (for me) quite tricky to pass an array as a parameter to the constructor.

I have the following on an include file.

Signal.h

#ifndef _SIGNAL_H_
#define _SIGNAL_H_

#include <Arduino.h>

const uint8_t numPins = 8;

/*
  Base class for signals
*/
class Signal
{
  protected:
    uint8_t (&_pins)[numPins];
    uint8_t _value;

    // initialize use a reference to an array
    Signal(uint8_t (&pins)[numPins]) : _pins(pins) {};

    // initialize use a pointer to an array
    Signal(uint8_t (*pins)[numPins]) : _pins(*pins) {};

  public:
    // begin method is shared in derived classes
    void begin();

    // this method differs in the derived classes
    virtual void show(uint8_t value);

    // shared with derived classes
    void printPins();
};

class SignalSimple : public Signal
{
  public:
    // initialize use a reference to an array
    SignalSimple(uint8_t (&pins)[numPins]) : Signal{&pins} {};

    // initialize use a pointer to an array
    SignalSimple(uint8_t (*pins)[numPins]) : Signal{*pins} {};

    void show(uint8_t value);
};

class SignalComplex : public Signal
{
  public:
    // initialize use a reference to an array
    SignalComplex(uint8_t (&pins)[numPins]) : Signal{&pins} {};

    // initialize use a pointer to an array
    SignalComplex(uint8_t (*pins)[numPins]) : Signal{*pins} {};

    void show(uint8_t value);
};
#endif

The question is about the use of the colon. I understand that in e.g. class SignalComplex : public Signal the colon indicates that SignalComplex is derived from Signal. But I do not know what the colon does in the constructor Signal(uint8_t (&pins)[numPins]) : _pins(pins) {};.

To complete the info

Signal.cpp

#include "Signal.h"


// begin method is shared with derived classes
void Signal::begin()
{
  for (uint8_t cnt = 0; cnt < sizeof(_pins); cnt++)
  {
    if (_pins[cnt] != 255)
    {
      pinMode(_pins[cnt], OUTPUT);
    }
  }
}

void Signal::printPins()
{
  for (uint8_t cnt = 0; cnt < sizeof(_pins); cnt++)
  {
    Serial.print(_pins[cnt]);
    Serial.print(",");
  }
  Serial.println();
}

void SignalSimple::show(uint8_t value)
{
  _value = value;
  Serial.print("setting _value in SignalSimple to ");
  Serial.println(value);
}

void SignalComplex::show(uint8_t value)
{
  _value = value;
  Serial.print("setting _value in SignalComplex to ");
  Serial.println(value);
}

And the sketch; it prints out the pins as I expect, no surprises there ( except for that it works :slight_smile: )

/*
  This is the first serious attempt at classes !!
  For passing an array to a constructor, code based on https://stackoverflow.com/questions/9426932/how-do-i-pass-an-array-to-a-constructor
  For using derived classes and passing an array to the constructor, code based on https://www.learncpp.com/cpp-tutorial/constructors-and-initialization-of-derived-classes/
*/

#include "Signal.h"

uint8_t pinsSimple[numPins] = {2, 3, 4, 255, 255, 255, 255, 255};
SignalSimple sbs(pinsSimple);

uint8_t pinsComplex[numPins] = {8, 9, 19, 11, 12, 13, 255, 255};
SignalComplex sbc(pinsComplex);

void setup()
{
  Serial.begin(57600);
  while (!Serial);

  sbs.printPins();
  sbs.begin();
  sbs.show(3);
  sbc.printPins();
  sbc.begin();
  sbc.show(45);
}

void loop()
{
}

Thanks in advance

Try this: C++, What does the colon after a constructor mean? - Stack Overflow
Note the top-voted answer. I think it answers your question as well. It's a way of initializing member variables based on arguments.

1 Like

:+1:

Thanks. I knew it was a way of initialising :wink:

So nothing to do with "derived from an array class" if I understand it correctly.

Yeah, that's my understanding as well. It's confusing sometimes, but in this case, it's just a distraction and indeed doesn't relate to the heart of the matter.

in C++ Programming Style, Cargill discusses poor class design and usage. Use of classes can often create nightmarish code.

the code you've posted is a poor example of a class why is there both a constructor and not one, but repeated calls to begin().

I can accept the fact that it's poor code :wink:

Two different objects of different derived classes and the show method will do different things. I might see it wrong.

I will try to read the link later.

why do you think there are "derived classes"?

sorry. i didn't look at the code closely enough with so many similar symbol names

not sure your question is about inheritance, passing array arguments, ...

arguments can be pointers which can represent arrays (e.g. c-string). (& sym prevents passing null pts)

the 3rd aspect is constructor vs begin(). seems that begin() make more sense if initialization needs to be postponed (e.g. after HW is initialized)

another aspect is the array is maintained external to the class. an alternative would be to have an object for each pin

programming has always been "religious" -- people advocating how things should be done. trivial examples are often confusing because they're so contrived. "bundling" data and function is an age old concept.

inheritance and virtual functions is the next level on the ladder. again, having a good example that needs it makes it easier to understand

Thanks for the extensive reply; plenty to study :wink:

I was about passing the array (and the use of the colon for that).

Interesting to know, I think I'm battling with that in another class that I'm writing; but that is for another topic.

The begin() is clearly hardware related :wink:

I wasn't happy with that either; I actually wanted to do what I do in structs (you can add function pointers to it simulating the virtual functions).

struct ABC
{
  const uint8_t pins[8];
  uint8_t value;
};

ABC someArray[] = 
{
  {{2,3,4}, 0},
  {{5,6,7}, 0},
};

C-style arrays are a mess, if you use a std::array, you can just pass it to the constructor by value or reference-to-const and initialize the member like any normal variable:

class Signal
{
  protected:
    std::array<uint8_t, numPins> pins;
    uint8_t value;

    // initialize use a reference to an array
    Signal(const std::array<uint8_t, numPins> &pins) : pins(pins) {}
1 Like

I don't agree :wink: But they might not have a place in C++ :slight_smile:

I will give it a shot later. I did give something like that a try but broke my neck over the #include (maybe I don't need a special one, maybe std, maybe std.h).

The problem is that they don't behave like other variables. You cannot copy them using normal assignment or compare them using the equality operator, you cannot initialize them in member initializer lists, you cannot return them from functions, and they have a nasty habit of decaying to pointers, discarding all size information.
std::array solves all of these issues.

If you're using an AVR, you don't have access to the standard library, so you'll have to install a third-party port, or create your own std::array replacement, in its most basic form, it's just:

template <class T, size_t N>
struct array {
  T data[N];
  T &operator[](size_t i) { return data[i]; }
  const T &operator[](size_t i) const { return data[i]; }
  T *begin() { return data; }
  const T *begin() const { return data; }
  T* end() { return data + N; }
  const T* end() const { return data + N; }
};

On all other platforms (ARM, ESP etc.) you can just #include <array>.

so why suggest a C++ concept that's not standard on Arduino? what about KISS

1 Like

Because it is the proper solution. It makes the rest of the code simpler if you can just use the right tool.
As I mentioned, C-style arrays cannot be initialized in member initializer lists, which seems to be the goal here, so you are forced to pass it by reference or by pointer and/or copy the contents manually.
C++ arrays have no such issues, so it is natural to use those in C++ code rather than messy C-style arrays.

Arduino is not limited to 8-bit AVR microcontrollers. Even if you're using an AVR, it's just 10 lines of code.

the "proper" solution (is ever other improper !!)

for the rest of the 50 line program ???

while i have no experience myself, i believe i understand how the more advance C++ concepts you suggest make a lot of sense in larger windows or very larger scientific applications, but i don't see their value in terms of the time needed to understand them (Stroustrup suggest 10 years!).

i understand why we all look for opportunities to learn about these advanced concepts. (Stroustrup also warned against using C++ concepts unnecessarily). but what i have found is that a time may come when the need for such a concept becomes obvious. it's one of those "that's where this makes sense" moments. you apply the concept and move on.

1 Like

When programming in C++ and when in need of an array that behaves like you would expect from a normal variable, you use a C++ std::array. It integrates nicely with the rest of the language, without the surprises you get from C-style arrays.
If you're used to programming in C, I can see why using C-style arrays would be your first instinct, but that doesn't mean that it's the best solution.

On AVR, you can argue about whether it's worth it.

There already are multiple tabs, you just copy and paste the 10 lines into a header file, either as a tab in your sketch or in your libraries folder. You just include it whenever you need it and don't look back. If it makes the rest of the code easier to read, that's well worth it IMO.

std::array is not an advanced concept.
It may be a new concept if you're used to C-style arrays, but it's easier to use, and definitely doesn't take 10 years to master.

don't know what this means (could you provide an example)

from my perspective i want the advantages of an index which is a disadvantage for non-indexed variables. (a linked list would be a similar example)

i'm puzzled by the example show in std::array. it seems the assumption is a need to sequentially access each array element.

i often use arrays of structs. not sure what the benefit of std::array would be in that case (although i have no doubts it could be used)

does it need to be "the best" solution? (is there one "right" way)?

since i often write in different languages, i prefer common concepts. i recognize languages have different advantages and try to take advantage of them (often reason for using a particular language)

maybe i missed it, but do you know in which edition of stroustrups books it was described?

i see it was C11 (2011)

Here are 6 scenarios where using C-style arrays has unexpected behavior (or simply doesn't work at all):

#include <cstddef>
#include <iostream>
#include <array>
#include <type_traits>

#define USE_STD_ARRAY 0

#if USE_STD_ARRAY
using array3i = std::array<int, 3>;
#else
using array3i = int[3];
#endif

#if USE_STD_ARRAY // these portions are disabled because they don't compile when using C-style arrays

// 1.
// C-style arrays cannot be returned from functions
array3i functionReturningArray() {
    array3i a = {1, 2, 3};
    return a;
}

#endif

// 2.
// Can C-style arrays be passed by value?
void fun(array3i array) { 
    array[0] = 42;
}

void test2() {
    array3i a = {1, 2, 3};
    fun(a);
    std::cout << a[0] << std::endl; // wrong result
    // The array a is now {42, 2, 3}, because the function operates
    // on the actual array, not on a copy of it. C-style arrays 
    // can never be passed by value.
}

// 3.
// C-style arrays as function arguments look exactly the same
// as variables of array type, but they secretly are pointers:
int sum(array3i array) {
    int sum = 0;
    size_t size = sizeof(array) / sizeof(array[0]);
    // Wrong: sizeof(array) is the same as sizeof(int*)
    for (size_t i = 0; i < size; i++)
        sum += array[i];
    return sum;
}

void test3() {
    array3i a = {1, 2, 3};
    int res = sum(a);
    std::cout << res << std::endl; // wrong result
}

#if USE_STD_ARRAY

// 4.
// C-style arrays cannot be assigned or copied like normal variables.
void test4() {
    array3i a = {10, 20, 30};
    array3i b = a; // error
}

#endif

#if USE_STD_ARRAY

// 5.
// C-style array members of a class or struct cannot be initialized
// in the constructor:
struct S {
    array3i a;
    S(const array3i &a)
        : a(a) {} // error: array used as initializer
};

#endif

// 6. 
// C-style arrays implicitly decay to pointers to the first element,
// they don't follow the usual value semantics.
void test6() {
    array3i a = {1, 2, 3};
    auto b = a;
    if (std::is_same<decltype(b), int *>::value)
        std::puts("b is a pointer to int?!");
}

int main() {
    test2();
    test3();
    test6();
}

(Compile using g++ -std=c++11 file.cpp)

If you set #define USE_STD_ARRAY 1, everything works as expected.

What do you mean? Of course you can index a std::array:

#include <array>
#include <iostream>

int main() {
  std::array<int, 3> arr = {1, 2, 3};
  int third_element = arr[2];
  std::cout << third_element <<  std::endl; // 3
  arr[0] = 42;
  std::cout << arr[0] << std::endl; // 42
}

An array is a common concept, and I would argue that std::array matches the array/list/vector types in other languages more closely than C-style arrays with their quirks.

To be absolutely clear, std::array is a drop-in replacement for C-style arrays, but without the weird behavior demonstrated in the code I posted at the top of this reply.

There might be more than one β€œright” way, but in this case, there is a clear problem: you cannot initialize an array member in a constructor. This problem can be trivially solved by using a std::array.

If you insist on using C-style arrays, you cannot initialize it directly, you have to memcpy the contents manually, specifying the size, with the caveat that it only works for TriviallyCopyable types, so you have to explain what those are, if the type is not trivially copyable, you have to resort to std::copy or write a for loop manually ... That's an awful amount of complexity just to initialize an array member.
Or you could use a std::array which just works like any other variable, in all cases without caveats.

sorry, but without some explanation i don't understand all these cases

i think in general you're making that the point that an array as a whole can't be treated as a monolithic singular variable

     int a [3] = { 1, 2, 3};
     int b [3];

     b = a

such as you might with a struct. i don't see this as a limitation.

these seem like similar discrepancies between c-strings and string.

sorry. it wasn't clear from what i saw.

doesn't that depend on the language?

don't understand. i love static initialization

// structure containing all servo parameters
struct Servo { 
    int     pinBut;
    int     servo;

    int     pulWidA;
    int     pulWidB;

    int     pinLedRed;
    int     pinLedGrn;

    int     stateServo;
    int     stateBut;
};


Servo servos [] = {
    {3,  0, 370, 285, 4,   5}, //Alton Siding 1
    {6,  1, 370, 285, 7,   8}, //Alton Siding 2
    {9,  2, 370, 285, 10, 11}, //Alton Siding 3
    {22, 3, 370, 285, 23, 24}, //Church's Falls 1
    {25, 4, 370, 285, 26, 27}, //Church's Falls 2
    {28, 5, 370, 285, 29, 30}, //Sawmill
    {31, 6, 370, 285, 32, 33}, //Turntable Entrance
    {34, 7, 370, 285, 35, 36}, //Church's Falls 3
    {37, 8, 370, 285, 38, 39}, //Crossover A
    {37, 9, 370, 285, 38, 39}, //Crossover B
    {40,10, 370, 285, 41, 42}, //Church's Falls 4
};

appreciate you're time trying to explain.

i think the difference between our perspectives is i grew around hardware. in college writing assembler code to access hardware register from pascal programs and have spent almost all of my career working on embedded projects.

it's common to use structs and array definitions, defining a pointer to such an definition, and setting it to the address of hardware. this is what C was designed for (e.g. a tty driver accessing UART registers) for these purposes, i want a very raw understanding of the variable definition. i don't have a problem with the limitations you describe because.

i think i can understand the desire for the features you describe in high level applications. of course, if you're familiar with std::array, then using it at a low-level is not a problems.