1

I would like to create a basic inheritance structure based on Halide::Generator in Halide/C++, so as to avoid duplicated code.

The idea is to have an abstract base generator class that owns a pure virtual function. Moreover, each derived class should have a specific input parameter, not available in the base class.

In normal C++ this is quite straightforward, but as Halide is a DSL that "generates code" before linking and compiling, things may get a little bit messy.

My current Halide implementation is all in a single file:

my_generators.cpp

#include "Halide.h"
#include <stdio.h>

using namespace Halide;

class Base : public Halide::Generator<Base> {
public:
    Input<Buffer<float>> input{"input", 2};

    Output<Buffer<float>> output{"brighter", 2};

    Var x, y;

    virtual Func process(Func input) = 0;

    virtual void generate() {
        output = process(input);
        output.vectorize(x, 16).parallel(y);
    }
};

class DerivedGain : public Base {
    public:
    Input<float> gain{"gain"};

    Func process (Func input) override{
        Func result("result");
        result(x,y) = input(x,y) * gain;
        return result;
    }
};

class DerivedOffset : public Base{
    public:
    Input<float> offset{"offset"};

    Func process (Func input) override{
        Func result("result");
        result(x,y) = input(x,y) + offset;
        return result;
    }
};

HALIDE_REGISTER_GENERATOR(DerivedGain, derived_gain)
HALIDE_REGISTER_GENERATOR(DerivedOffset, derived_offset)

In order to compile it, I have used this CMakeLists file:

cmake_minimum_required(VERSION 3.16)
project(HalideExample)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)

find_package(Halide REQUIRED)

add_executable(my_generators my_generators.cpp)

target_include_directories(my_generators PUBLIC ${HALIDE_ROOT}/include)
target_link_libraries(my_generators PRIVATE Halide::Generator)

add_halide_library(derived_gain FROM my_generators)
add_halide_library(derived_offset FROM my_generators)

I have been using the pre-built version Halide-13.0.1-x86-64-linux here

But during compilation, it launches an error suggesting that class Base was being instantiated (which I do not need to happen) :

In file included from <path_to_project>/my_generators.cpp:2:
<path_to_halide>/include/Halide.h: In instantiation of ‘static std::unique_ptr<_Tp> Halide::Generator<T>::create(const Halide::GeneratorContext&) [with T = Base]’:
<path_to_halide>/include/Halide.h:26640:14:   required from ‘static std::unique_ptr<_Tp> Halide::Generator<T>::create(const Halide::GeneratorContext&, const string&, const string&) [with T = Base; std::string = std::__cxx11::basic_string<char>]’
<path_to_project>/my_generators.cpp:53:1:   required from here
<path_to_halide>/include/Halide.h:26631:37: error: invalid new-expression of abstract class type ‘Base’
26631 |         auto g = std::unique_ptr<T>(new T());
      |                                     ^~~~~~~
<path_to_project>/my_generators.cpp:7:7: note:   because the following virtual functions are pure within ‘Base’:
    7 | class Base : public Halide::Generator<Base> {
      |       ^~~~
<path_to_project>/my_generators.cpp:15:18: note:    ‘virtual Halide::NamesInterface::Func Base::process(Halide::NamesInterface::Func)’
   15 |     virtual Func process(Func input) = 0;

If instead of using a virtual function I implement it in the Base class like so:

class Base : public Halide::Generator<Base> {
public:
    Input<Buffer<float>> input{"input", 2};

    Output<Buffer<float>> output{"brighter", 2};

    Var x, y;

    // Func process(Func input);
    Func process (Func input){
        Func result("result");
        result(x,y) = input(x,y);
        return result;
    }

    virtual void generate() {
        output = process(input);
        output.vectorize(x, 16).parallel(y);
    }
};

Then everything compiles, but the object and header files with the generated code have the wrong function signatures (noticeable as there are missing gain/offset parameters):

derived_gain.h:

int derived_gain(struct halide_buffer_t *_input_buffer, struct halide_buffer_t *_result_buffer);

derived_offset.h:

int derived_offset(struct halide_buffer_t *_input_buffer, struct halide_buffer_t *_result_buffer);

Therefore, I would like to know which mistake I am introducing in the class definitions and how to solve it.

tuscasp
  • 49
  • 5
  • Please first extract a [mcve] and include that in your question. Also, if you want to create a derived class instance, that implies that you also create a base class instance, because it has a base class instance subobject. So, you need the constructor, obviously, either implicitly generated or written manually. – Ulrich Eckhardt Nov 22 '21 at 12:46
  • I have updated the issue. Now I make it clear that what I want is an abstract base class. But still, the issue with the object and header files generated by Halide remains. – tuscasp Nov 22 '21 at 15:17
  • I'm still not sure what exactly you're compiling and how. That said, one thing I'd look at is the `public Halide::Generator` baseclass. It could be that this is what's wrong, and that the registration macros then try to generate a `Base`, which is what the error is all about. If in doubt, check out what the macro resolves to. – Ulrich Eckhardt Nov 22 '21 at 15:48

1 Answers1

4

You could turn the base class into a template:

template<class T>
class Base : public Halide::Generator<T> {

and then re-export the Input and Output names... (I'm not enough of a C++ guru to understand why this is necessary):

  // In class Base:
  template <typename T2>
  using Input = typename Halide::Generator<T>::template Input<T2>;

  template <typename T2>
  using Output = typename Halide::Generator<T>::template Output<T2>;

Then the remaining changes are just:

class DerivedGain : public Base<DerivedGain> { ... };
class DerivedOffset : public Base<DerivedOffset> { ... };

This seemed to work for me.


Also, you probably do not need this line in your CMakeLists.txt (I didn't):

target_include_directories(my_generators PUBLIC ${HALIDE_ROOT}/include)

Our package doesn't set HALIDE_ROOT, and linking to Halide::Generator already sets up the include paths correctly, anyway.

Alex Reinking
  • 16,724
  • 5
  • 52
  • 86
  • 1
    I think this is why. [Class templates and inheritance](https://www.modernescpp.com/index.php/surprise-included-inheritance-and-member-functions-of-class-templates). Which links to [Two phase lookup](https://stackoverflow.com/questions/7767626/two-phase-lookup-explanation-needed). Ie, the template variable needs an annotation of `using` so that is not ambiguous. The page discusses functions, but I think the resolution applies to class variables as well... and I read [these web pages](https://www.modernescpp.com/index.php) in order to try and understand Halide (paradoxically). – artless noise Nov 30 '21 at 18:11
  • @artlessnoise -- thanks for the links! – Alex Reinking Nov 30 '21 at 18:17