A different approach toward Conditional Compilation in Libraries

Lets say that we want to create a dynamic Library with properties that are predefined in the sketch so that things like CPU or RAM usage would be optimised based on user's preference [...] What i would like to ask you guys, is for your opinion about this trick i found out (for solving this exact issue), have you ever seen or used it before? and if so, does it effect in any negative way the library?

DynamicLibrary.h

...

class DynamicLibrary
{
    ...

    #if !defined(OPTIONS)
      void Function1(); // default
      void Function1(bool O1); // Optimized for SRAM
      void Function1(bool O1 , bool O2); // Optimized for CPU
    #else
      #if defined(RAM)
        void Function1(bool O1 = true);
      #elif defined(CPU)
        void Function1(bool O1 = true , bool O2 = true);
      #else
      //default?
      #endif
    #endif
}

DynamicLibrary.cpp

...

void DynamicLibrary::Function1()
{
    ...
    Serial.println("default");
}

void DynamicLibrary::Function1(bool O1)
{
    ...
    Serial.println("...SRAM");
}

void DynamicLibrary::Function1(bool O1, bool O2)
{
    ...
    Serial.println("...CPU");
}

Sketch.ino

#define OPTIONS
#define RAM // Optimised for SRAM
...

#include <DynamicLibrary.h>

void setup()
{
    DynamicLibrary DL(...);
  
    DL.Function1();
}
...

Thanks in Advance for any opinion <3

That won't work. The options macros are not defined when including the header file from the .cpp file, only when including it from the sketch.
This violates the ODR (one definition rule), because you define the same class twice, with different contents.

To pull this off, you'd have to add a global compilation flag (hard to do when using the Arduino IDE), or make your library header-only.

Pieter

PieterP:
That won't work.

No joke, I know it seems unbelievable but it works, i tested it. Try it.

giorgos_xou:
it works, i tested it. Try it.

It might work in this case because you're only calling the member functions from your sketch. Once you start calling some of the member functions from the .cpp file, you'll find that it won't work anymore.

Even if it works on your hardware, with your specific compiler and optimization level, your program violates the ODR, so there's no guarantee that it'll work in other circumstances.

PieterP:
with your specific compiler and optimization level

I use Arduino 1.8.9 IDE not any specific compiler

PieterP:
Once you start calling some of the member functions from the .cpp file, you'll find that it won't work anymore.

it works, tested it :stuck_out_tongue:

See Definitions and ODR (One Definition Rule) - cppreference.com

One Definition Rule

There can be more than one definition in a program of each of the following: class type, enumeration type, inline function, templated entity (template or member of template, but not full template specialization), as long as all of the following is true:

  • each definition appears in a different translation unit

  • each definition consists of the same sequence of tokens (typically, appears in the same header file)

  • name lookup from within each definition finds the same entities (after overload-resolution), except that

  • constants with internal or no linkage may refer to different objects as long as they are not ODR-used and have the same values in every definition

  • lambda-expressions that are not in a default argument are uniquely identified by the sequence of tokens used to define them

  • overloaded operators, including conversion, allocation, and deallocation functions refer to the same function from each definition (unless referring to one defined within the definition)

  • ...

If all these requirements are satisfied, the program behaves as if there is only one definition in the entire program. Otherwise, the program is ill-formed, no diagnostic required.

Your cod violates the second and third bullet.

giorgos_xou:
I use Arduino 1.8.9 IDE not any specific compiler

The version of the Arduino IDE is irrelevant. You have so-called "Cores" installed, see the "board manager". Each "Core" comes with a toolchain for some architecture, and that toolchain includes a specific version of a compiler. For example for the "ArduinoCore-AVR v1.8.3" that's used for the Arduino UNO, this is currently avr-gcc 7.3.0.

giorgos_xou:
it works, tested it :stuck_out_tongue:

No, it doesn't. Try this:

#pragma once

class DynamicLibrary {
  public:
    #if !defined(OPTIONS)
      void Function1(); // default
      void Function1(bool O1); // Optimized for SRAM
      void Function1(bool O1 , bool O2); // Optimized for CPU
    #else
      #if defined(RAM)
        void Function1(bool O1 = true);
      #elif defined(CPU)
        void Function1(bool O1 = true , bool O2 = true);
      #else
      //default?
      #endif
    #endif

    void do_the_default_thing();
};
#include "DynamicLibrary.h"
#include <Arduino.h>

void DynamicLibrary::Function1() {
    Serial.println("default");
}

void DynamicLibrary::Function1(bool O1) {
    Serial.println("...SRAM");
}

void DynamicLibrary::Function1(bool O1, bool O2) {
    Serial.println("...CPU");
}

void DynamicLibrary::do_the_default_thing() {
    Function1();
}
#define OPTIONS
#define RAM // Optimised for SRAM

#include "DynamicLibrary.h"

void setup() {
    Serial.begin(115200);
    while (!Serial);
    
    DynamicLibrary dl;
    dl.Function1();
    dl.do_the_default_thing();
}

void loop() {}
...SRAM
default

When you call "Function1" from the .cpp file, it doesn't see the options you've set in your sketch, so it always runs the default function, not the one optimized for RAM, even though that option was set in the sketch. When you call the same function from the sketch, you do get the the optimized version. This clearly violates the ODR.

PieterP:
No, it doesn't.

You are right. Now i see what you mean in this specific case, but i still see the potential of this method being used in some other cases. Is it really that wrong if i want to use it just like i used it, only from the skectch or should i approach it in another way?

i i actually decide to use this method

PieterP:
there's no guarantee that it'll work in other circumstances.

Ηowever, one more question: if i actually decide to use this "trick" method, is it something that could be eliminated/stop-being-supported in the next versions of avr-gcc or it is safe for someone to use it as a feature?

Also, how about this:

#pragma once

class DynamicLibrary {
  public:
    #if !defined(OPTIONS)
      void Function1(); // default
      void Function1(bool O1); // Optimized for SRAM
      void Function1(bool O1 , bool O2); // Optimized for CPU

      void do_the_default_thing();
      void do_the_default_thing(bool O1);
      void do_the_default_thing(bool O1 , bool O2);
    #else
      #if defined(RAM)
        void Function1(bool O1 = true);
        void do_the_default_thing(bool O1 = true);
      #elif defined(CPU)
        void Function1(bool O1 = true , bool O2 = true); 
        void do_the_default_thing(bool O1 = true, bool O2 = true);
      #else
      //default?
      #endif
    #endif

    
};

The main problem here is that the implementation files (all .cpp files and the .ino sketch) are all compiled independently.
The #include directive is just a straight copy-paste of the contents of the header file into the implementation file. Together with the macros you're using and the other preprocessor directives (everything that starts with #), they are handled by the preprocessor, before the code is passed to the compiler. The compiler never sees your option macros.

This means that the .cpp file always sees the default version of your class, and it is compiled completely separately from the sketch, so it isn't affected by the options that might be defined in the sketch, it doesn't even know the sketch exists. The compiler compiles the .cpp file to an object file.

On the other hand, your .ino file only sees the version of the class with the specified options. The sketch is also compiled to a separate object file.

Then both object files are stitched together by the linker. The compiler never sees the two files together, so it has no idea that there are two different versions of your "DynamicLibrary" class.*

In conclusion, there is no way to communicate the settings in your sketch to the other .cpp files.

AFAIK, the only way is to provide a global macro definition of your options, either as a compiler flag (-DOPTIONS -DRAM), but this is almost impossible to do with the Arduino builder, or by defining them inside of a "Config.h" header file that's included in DynamicLibrary.h. That does of course make it harder for users of the library to change the optimization options, they'll have to edit a file inside of the library directory to do so.

If you know for a fact that your library will only be included in the main .ino sketch file, you could use a header only library, with options that are defined before including it (like what you're doing right now, but without the .cpp file). If you or a user includes your header-only library in one or more .cpp files, however, you end up with exactly the same problem as you have now.

(*) This isn't entirely true, since most Arduino Cores compile sketches with link-time optimizations enabled, so it does invoke the compiler at link time in this case, but even then, the compiler doesn't check whether there are different versions of you class.

giorgos_xou:
You are right. Now i see what you mean in this specific case, but i still see the potential of this method being used in some other cases. Is it really that wrong if i want to use it just like i used it, only from the skectch or should i approach it in another way?

Yes, it is really wrong. See the quote I posted earlier: the program is ill-formed, no diagnostic required. Your program is not valid if you use this method, and the compiler doesn't have to tell you that it's not valid, it just does "something", but there's no way to know what it'll do. It might generate an empty program, it might generate a program that crashes, it might generate a program that works on some architectures but not on others ...
It might work by "chance" with some compilers and options, it might suddenly break if you add or remove code, because the compiler decides to optimize your code differently, inlining some functions, etc.
You cannot rely on it, I wouldn't ever use it in a critical project or a public library.

giorgos_xou:
i i actually decide to use this method
Ηowever, one more question: if i actually decide to use this "trick" method, is it something that could be eliminated/stop-being-supported in the next versions of avr-gcc or it is safe for someone to use it as a feature?

It is simply not supported, the fact that it works now is just chance. There's a very real possibility that it'll break when the Arduino team decides to update the compiler.

giorgos_xou:
Also, how about this:

#pragma once

class DynamicLibrary {
  public:
    #if !defined(OPTIONS)
      void Function1(); // default
      void Function1(bool O1); // Optimized for SRAM
      void Function1(bool O1 , bool O2); // Optimized for CPU

void do_the_default_thing();
      void do_the_default_thing(bool O1);
      void do_the_default_thing(bool O1 , bool O2);
    #else
      #if defined(RAM)
        void Function1(bool O1 = true);
        void do_the_default_thing(bool O1 = true);
      #elif defined(CPU)
        void Function1(bool O1 = true , bool O2 = true);
        void do_the_default_thing(bool O1 = true, bool O2 = true);
      #else
      //default?
      #endif
    #endif

};

It still violates the C++ standard, and adds even more complexity. You simply cannot do it this way using macros, see the alternatives I mentioned above.

Thanks you very much for taking the time to answer all of my questions, i really apreaciate your advice and help. Although i am quite disapointed :frowning: that the only real "work around" is writing the whole code in the header file

I wrote a library that needs a user controlled allocation, you just pass the buffer size that you want and the constructor allocates it:

class TerminalServer {
public:
	TerminalServer(int inputBufferSize) :
			inputLineComplete(true), index(0) {
		receivedChars = new char[inputBufferSize];
	}

in a sketch:

TerminalServer term(COLUMNS_80);

creates a console that uses an 80 character buffer.

Maybe I missed the point, as this is a simple example?

aarg:
Maybe I missed the point, as this is a simple example?

Hahahaha, nice try!

Mainly what i am trying, is to use multiple constructor functions with the same variables but diferent algorithmic properties.... The same goes with the other functions in the library... plus etc.

PieterP:
AFAIK, the only way is to provide a global macro definition of your options, either as a compiler flag (-DOPTIONS -DRAM), but this is almost impossible to do with the Arduino builder

Efforts have actually been made to make this reasonably easy to do if you are using Arduino CLI or the Arduino IDE command line. There are "extra flags" build properties that allow the compilation commands to be customized by adding arbitrary options. The build properties can be set via command line options.

So you can do things like this:
Arduino IDE

arduino --pref compiler.cpp.extra_flags="-DOPTIONS -DRAM" --board arduino:avr:uno --verify Sketch/Sketch.ino

Arduino CLI

arduino-cli compile --build-properties=compiler.cpp.extra_flags="-DOPTIONS -DRAM" --fqbn arduino:avr:uno Sketch

Thanks pert but most probably i will use the heder file method

pert:
Efforts have actually been made to make this reasonably easy to do if you are using Arduino CLI or the Arduino IDE command line. There are "extra flags" build properties that allow the compilation commands to be customized by adding arbitrary options. The build properties can be set via command line options.

So you can do things like this:
Arduino IDE

arduino --pref compiler.cpp.extra_flags="-DOPTIONS -DRAM" --board arduino:avr:uno --verify Sketch/Sketch.ino

Arduino CLI

arduino-cli compile --build-properties=compiler.cpp.extra_flags="-DOPTIONS -DRAM" --fqbn arduino:avr:uno Sketch

Thanks, I wasn't aware of that!

Do you know if there are any plans to allow setting these options in the library.properties file or something similar?
In some cases, I think it would be very useful to have a "debug" or "verbose" option for a specific library, for example (similar to the options you have in the boards.txt files, e.g. the Espressif cores use it to provide an option to enable warnings and debug output from the core libraries).

On the other hand, this could of course lead to libraries adding way too many options, making them harder to use for beginners ...

giorgos_xou:
Thanks pert but most probably i will use the heder file method

That's what I would do (and have done).

I was pointing it out more to dispute the "almost impossible" claim than to actually recommend it.

I love having a command line interface for scripts (and in that application I would definitely use the --build-properties option) but I don't often use the command line directly. I also would never want to require the users of code I share to use the command line.

It might be useful for something that isn't really a standard user interface, like enabling debug output from a library.

with
compiler.cpp.extra_flags="-DOPTIONS -DRAM"

it would not compile. functions in cpp would be missing declarations in h