Linker error when trying to compile multi-file c++ project

I have a project structured like:

  • .pio
  • .vscode
  • include
  • lib
  • src
    • main.cpp
    • scheduler.hpp
    • scheduler.cpp
    • … other files
  • test
  • platformio.ini
  • .gitignore

My platformio.ini file is:


[env]
; GDB stub implementation
lib_deps =
    jdolinay/avr-debugger @ ~1.1

[env:nanoatmega328]
platform = atmelavr
board = nanoatmega328
framework = arduino
debug_tool = avr-stub
debug_port = /dev/ttyUSB0

I’m receiving the following error:


Processing nanoatmega328 (platform: atmelavr; board: nanoatmega328; framework: arduino)
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Verbose mode can be enabled via `-v, --verbose` option
CONFIGURATION: https://docs.platformio.org/page/boards/atmelavr/nanoatmega328.html
PLATFORM: Atmel AVR (3.1.0) > Arduino Nano ATmega328
HARDWARE: ATMEGA328P 16MHz, 2KB RAM, 30KB Flash
DEBUG: Current (avr-stub) On-board (avr-stub, simavr)
PACKAGES: 
 - framework-arduino-avr 5.1.0 
 - toolchain-atmelavr 1.50400.190710 (5.4.0)
LDF: Library Dependency Finder -> http://bit.ly/configure-pio-ldf
LDF Modes: Finder ~ chain, Compatibility ~ soft
Found 6 compatible libraries
Scanning dependencies...
Dependency Graph
|-- <avr-debugger> 1.1.0
Building in release mode
Linking .pio/build/nanoatmega328/firmware.elf
/tmp/ccTrCSwd.ltrans0.ltrans.o: In function `main':
<artificial>:(.text.startup+0x1b4): undefined reference to `Scheduler<2, 1000, 3600000ul>::initializeSensors() const'
<artificial>:(.text.startup+0x1bc): undefined reference to `Scheduler<2, 1000, 3600000ul>::tick()'
collect2: error: ld returned 1 exit status
*** [.pio/build/nanoatmega328/firmware.elf] Error 1

What configuration option am I missing?

It says this function is not implemented, a code error, not a configuration error.

What is the exact contents of the files?

main.cpp

#include <Arduino.h>
#include "hygrometer.hpp"
#include "scheduler.hpp"

const Hygrometer hygrometers[2] = {
  {2, A0, spin_up_period, minutes(10)},
  {3, A1, spin_up_period, minutes(15)}
};
Scheduler<2> scheduler(hygrometers);

void setup() {
  Serial.begin(9600);
  scheduler.initializeSensors();
}

void loop() {
  scheduler.tick();
}

scheduler.hpp

#pragma once
#include "hygrometer.hpp"
#include "time-helpers.hpp"

// a quick-and-dirty scheduler.
// NOT real-time, causes drift.
template <
  // How many sensors will be scheduled.
  int sensorCount,
  // How long to wait before checking for a sensor read to be scheduled.
  int tickLength = seconds(1),
  // Roll over at 60 instead of 255 to simulate an hour-like rotation.
  // (due to drift this will be slightly more than an hour, but it can
  // still be reasoned about as though it were actually an hour)
  unsigned long rolloverTime = hours(1)
>
class Scheduler {
private:
  void _healthCheck() const;
  const Hygrometer* hygrometers;
  // The number of ticks which have passed
  unsigned long tickCount = 0;

  void wait(const int time = tickLength);
public:
  Scheduler(const Hygrometer* hs) : hygrometers(hs) {}

  void tick();
  void initializeSensors() const;
};

scheduler.cpp

#include <Arduino.h>
#include "scheduler.hpp"
#include "healthCheck.hpp"

template <int sensorCount, int tickLength, unsigned long rolloverTime>
void Scheduler<sensorCount, tickLength, rolloverTime>::_healthCheck() const {
  healthCheck(this);
}

template <int sensorCount, int tickLength, unsigned long rolloverTime>
void Scheduler<sensorCount, tickLength, rolloverTime>::wait(const int time) {
  delay(time);
  tickCount++;
  if (tickCount > rolloverTime) tickCount = 1;
}

template <int sensorCount, int tickLength, unsigned long rolloverTime>
void Scheduler<sensorCount, tickLength, rolloverTime>::tick() {
  _healthCheck();
  if (tickCount == 0)
    for (size_t i = 0; i < sensorCount; i++)
      hygrometers[i].showInput();
  else
    for (size_t i = 0; i < sensorCount; i++) {
      const auto position = long(tickLength) * tickCount;
      const long rem = position % hygrometers[i].cooldownPeriod;
      // Serial.print("# pos: ");             Serial.print(position);
      // Serial.print(", cooldown period: "); Serial.print(hygrometers[i].cooldownPeriod);
      // Serial.print(", remainder: ");       Serial.println(rem);
      if (rem == 0)
        hygrometers[i].showInput();
    }

  // Because tickLength and spinupTime are equal rn, there WILL be time drift.
  // Something scheduled to happen every 15 minutes might happen every 15 minutes,
  // or every 15 minutes and 30 seconds, depending on whether sensors are
  // configured to run in between those "15 minute" delays.
  // I could skip some ticks here, but that will lead to some sensors being
  // skipped sometimes, or a bunch of extra time spent calculating how much time
  // was spent and making it up without losing time, but really, this isn't a
  // "real-time" application so lets KISS.
  wait();
}

template <int sensorCount, int tickLength, unsigned long rolloverTime>
void Scheduler<sensorCount, tickLength, rolloverTime>::initializeSensors() const {
  for (size_t i = 0; i < sensorCount; i++) {
    pinMode(hygrometers[i].power, OUTPUT);
    pinMode(hygrometers[i].input, INPUT);
  }
}

I thought template implementations had to be done in the hpp file? Let me check…

Well I can’t check since the example is incomplete. hygrometer.hpp, time-helpers.hpp and healthCheck.hpp and the related cpp files are missing.

here is the project as an archive

I was trying to keep the post to relevant information.

I suspect you’re right about the issue being the template methods.

I solved the problem by removing the .cpp files (and inlining their contents into the headers) and removing the healthCheck.{h,c}pp files from the project (and inlining that function into the definition of Scheduler). It appears that you were right about needing to define methods on template classes within the header file. Thanks for your help @maxgerhardt