Home > Back-end >  C 20 how to have concept require a free function that is defined later
C 20 how to have concept require a free function that is defined later

Time:12-27

Suppose I have a concept MyConcept that describes the requirements for a generic algorithm algo. In the examples code below, the only requirement is a free function my_free_function that takes a MyConcept type and returns the same and there are three types that satisfy the requirement.

template<class T>
concept MyConcept = requires (T a) {
    { my_free_function(a) } -> std::same_as<T>;
};

template<MyConcept T>
auto algo(T& a) {
    return my_free_function(a);
}

namespace baz {
struct bar {};
bar my_free_function(bar) { return bar{}; }
}; // baz

unsigned long my_free_function(unsigned long) { return 42; }

namespace caz {
using NativeU64 = unsigned long;
NativeU64 my_free_function(NativeU64) { return NativeU64{42ul}; }
}; // caz

My challenge is that in order to get the following calls to compile for NativeU64 and unsigned long, I have to define my_free_function before the concept and algo. Is that the only alternative? My current code organization and dependency structure rely on being able to define a concept and algorithm in header files which are consumed by the compiler before seeing the definitions that will satisfy the requirements.

auto a = baz::bar{};
algo(a);

auto b = 1ul;
algo(b);

caz::NativeU64 c = 1ul;
algo(c);

The compiler error message is:

'my_free_function(a)' would be invalid: call to function 'my_free_function' that is neither visible in the template definition nor found by argument-dependent lookup { my_free_function(a) } -> std::same_as;

This matches up with what I have do to to get it to work, but I must be missing something.

CodePudding user response:

This is essentially why traits were invented: to allow extensions for types that cannot use ADL to extend their functionality. Concepts don't change these rules.

If you cannot declare these functions before the concept, then you're going to have to employ some kind of trait. The default trait can call the free function (and use a concept to make sure that the free function is available), while the specialized versions can call whatever they want.

That having been said, the genesis of your problem is already... dubious. See, unsigned long does not belong to you; it belongs to the system. Anybody can use that type at any time, for any reason. Which means that, so long as someone has access to MyConcept, they can ask if MyConcept<unsigned long> is true. That answer has to be based on whatever declarations are available at the time the question is asked.

So if a user has access to both MyConcept and unsigned long, it is actively dangerous for the user to be able to ask that question without you already having provided a specialized interface for unsigned long. Why?

Because if they ask it without including the declaration for your interface, they will get the wrong answer. And if you do anything which causes MyConcept<T> to give a different answer, your code is ill-formed (no diagnostic required), just like a violating of the one-definition rule.

Your code should make it impossible to do this. For types you define, that's easy: right after each definition, you include the interfaces that MyConcept is looking for. But as previously stated, unsigned long does not belong to you; anybody can use it at any time. Therefore, the same file that defines MyConcept should also define any default functionality that MyConcept relies on. Like the unsigned long version.

CodePudding user response:

C can't find the identifier 'my_free_function' (with or without concepts).

For C -Templates applies Two-Phase-Lookup (i.e. names are looked up twice):

  1. During parsing of the template
  2. During the instantiation of the template (at the point of instantiation; POI)
namespace {

    // Parsing
    // Look-up for non-dependent names:
    //  - not applicable for my_free_function
    // Ordinary Look-up for unqualified, dependent names:
    //  - my_free_function is unqualified, dependent (because of T& a as argument) name; not found with ordinary name look-up (result not definite)
    template<typename T>
    auto algo(T& a) {
        return my_free_function(a);
    }

    unsigned long my_free_function(unsigned long) { return 42; }
    std::string my_free_function(std::string) { return "42"; }

    namespace baz {
        struct bar {};
        bar my_free_function(bar) { return bar{}; }
    }; // baz

}

int main() {

    auto a = baz::bar{};
    algo(a);

    unsigned long b = 123ul;
    algo(b); // ERROR: 'my_free_function' identifier not found

    return 0;
}

/*
* POI for algo<baz::bar>(b)
*
* Instantiation:
*   baz::bar algo(baz::bar& b) {
*       return my_free_function(b);
*   }
*
* ADL for unqualified, dependent names:
*   - applicable for: my_free_function(b) (b has type baz::bar&)
*   -> does find baz::my_free_function
*
*/

/*
* POI for: algo<unsigned long>(b)
*
* Instantiation:
*   unsigned long algo(unsigned long& b) {
*       return my_free_function(b);
*   }
*
* Look-ups for qualified, dependent names:
*   - not applicable for: my_free_function(b) (b has type unsigned long)
* ADL for unqualified, dependent names (already looked up when parsing but were not found):
*   - not applicable for: my_free_function(b) (b has type unsigned long)
*
* -> my_free_function was not found!
*/

The case for NativeU64 is analogous to the unsigned long case. using NativeU64 = unsigned long; does not introduce a new type; NativeU64 is unsigned long.

When you declare the function before the template, it is found while parsing the template. Another solution would be using:

auto algo(unsigned long a) {
    return my_free_function(a);
}

after my_free_function(unsigned int) was declared so the template is never used for unsigned long.

See 'C Templates: The Complete Guide, 2nd Edition', especially chapter '14.3.1 Two-Phase Lookup'.

  • Related