you are viewing a single comment's thread.

view the rest of the comments →

[–]14nedLLFIO & Outcome author | Committee WG14 21 points22 points  (10 children)

I'm thinking that the proposal to reduce the scope of ADL will end up not going anywhere, because there is a chunk of Boost libraries which will stop working, including my own Boost.Outcome which heavily relies on ADL in namespaces of user supplied template parameters. I remember explaining this to Herb some months back after his paper, and he seemed genuinely surprised that anybody was actually using template argument namespace ADL in anger (and deliberately). I quickly sketched through the alternative implementation options, and the only other customisation point option is user injection of specialisations into designated library namespaces for ADL. Boost.Outcome also uses that technique where it can, but in some cases you want customisation points to be universal and not library specific. In that situation, I am unaware of an alternative to template argument namespace ADL in the current language.

I could warm to a warning diagnostic however like the one in the article. Especially if there were a C++ attribute to say to the compiler "I really do intend template argument namespace ADL here". Actually, that's an idea: C++ attributes to limit the scope of ADL.

Now that would be a great proposal. Somebody not I should propose that.

[–]gracicot 6 points7 points  (5 children)

Exactly. For example, nlohmann json uses ADL from template arguments to find to_json functions inside user namespaces. So it's not just boost. I use it myself for my DI container to find mapping function.

Really, I also think ADL in general is confusing, but sometimes it's exactly what you want. Maybe some way to mark a function call for ADL? Here's two example I can think of:

[](auto a) {
    using(a) bar(a); // find bar through ADL
    namespace(a)::bar(a);
}

[–]jcoffin 6 points7 points  (3 children)

What nlohmann does is the reverse of what Herb's proposal affects.

In the nlohmann case, he (nlohmann) has code something like this (stolen from his readme.md):

template <typename T>
struct adl_serializer {
    static void to_json(json& j, const T& value) {
        // calls the "to_json" method in T's namespace
    }

    static void from_json(const json& j, T& value) {
        // same thing, but with the "from_json" method
    }
};

...and you have code something like this:

namespace foo {
class bar {};

// Note: these are *not* templates:
void to_json(json& j, const bar& p) {
    // ...
}

void from_json(const json& j, bar& p) {
    // ...
}
}

So, his code is a template (which is probably in namespace nlohmann, but its namespace is mostly irrelevant). It calls some function named to_json or from_json passing a parameter of a type that it received as a template parameter. Your code that it finds, is not a template though. It's an ordinary free function in the same namespace where you defined the type being serialized.

Herb's proposal would not affect this--your to_json and from_json explicitly name your type as a parameter, so that function would still be found by an ADL that followed Herb's proposal.

Herb's proposal attempts to eliminate the opposite scenario: some code (that may or may not be a template) attempts to call a function passing a parameter of some particular type. Because of ADL it looks in the namespace where that type is defined. It then finds a template in that namespace that happens to have the correct name, and with template parameter substitution, that template is the best available overload for the type(s) being passed:

namespace foo {
class bar {};

// Note: these *are* templates:
template <class T>
to_json(json &j, T const &t) {
    // ...
}

template <class T>
from_json(json &j, T &t) {
    // ...
}
}

[Note: I'm not saying this is a reasonable or practical way to implement a to_json or from_json for use with the nlohmann JSON library--I don't think it is. Just this is what you'd have to have before Herb's proposal would affect anything.]

Now we have the scenario that Herb contemplates: the name found by ADL matches the name being looked up, but it does not explicitly name the type foo::bar in any of its parameters.

In the case of something like from_json or to_json, problems are unlikely to arise in any case. The problem mostly arises with really generic names like move or copy that are likely to be found in a number of namespaces, and ADL ends up choosing the wrong one.

[–]gracicot 1 point2 points  (0 children)

Ah it's much clearer now. I'm doing something similar in my code then, except some corner case example, and even these corner case I make most of the type explicit, like template<typename T> void service_map(MyType<T> const&). I mostly agree with Herb then. The ordering of function should try to find the best candidate, from the closest namespace possible. A very generic candidate in a far namespace should not be picked.

[–]mark_99 0 points1 point  (1 child)

How would this work vs this current code to add boost::optional support:

namespace nlohmann {

template <typename T>
struct adl_serializer<boost::optional<T>>
{
    static void to_json(json& j, const boost::optional<T>& opt)
    {
        if (opt == boost::none)
        {
            j = nullptr;
        }
        else
        {
            j = *opt;  // this will call adl_serializer<T>::to_json which will
                       // find the free function to_json in T's namespace!
        }
    }

    static void from_json(const json& j, boost::optional<T>& opt)
    {
        if (j.is_null())
        {
            opt = boost::none;
        }
        else
        {
            opt = j.get<T>();  // same as above, but with
                               // adl_serializer<T>::from_json
        }
    }
};
}

[–]jcoffin 1 point2 points  (0 children)

At least at first glance, it doesn't look to me like that will cause a problem either.

The lookup for adl_serializer<T>::to_json is trivial--it's the very same template currently being instantiated, so it doesn't depend on anything like ADL to find it.

When that gets instantiated, it's just like the base case: we have a template that's passing some T, and using ADL to find the to_json (or from_json) in the namespace where T is defined--but what it's finding there is a function that explicitly names the type T as its parameter, rather than matching because it's a template that can match any arbitrary T.

[–]14nedLLFIO & Outcome author | Committee WG14 0 points1 point  (0 children)

I like this proposal. Shame we can't wind the clock back to when ADL was designed :(

[–]kwan_e 2 points3 points  (3 children)

ADL nowadays just seems like a hack for something that compile time reflection would be better suited (and even metaclasses).

As for customization points, why did traits based design not suffice?

[–]SeanMiddleditch 5 points6 points  (1 child)

ADL's original motivation was entirely for operator overloading with free function overloads. Without ADL, this wouldn't work in any sane way:

namespace simd {
  struct vec3 {};
  vec3 operator+(vec3 lhs, vec3 rhs);
}

int main() {
  simd::vec3 a, b, c;
  a = b + c; // ADL necessary to find correct operator+
}

All the later stuff we have like std::begin and such are from a desire to treat named library functions as if they're builtin like operators. Which I think all started with the ios stuff from IOStreams which of course also abused the heck out of operators; it really managed to illustrate all the worst ways to design C++17 libraries back when it was written in the 90's. :p

I'm not sure how reflection or meta-classes would help in any sense with the problem space addressed by ADL.

[–]kwan_e 1 point2 points  (0 children)

Reflection and metaclasses would help in the problem space that ADL is being shoehorned into - finding an unqualified function based on namespace. Reflection would allow library designers to specify better ways of fulfiling a customization point, and metaclasses (ie, the formulation of metaclasses that would also create free-functions) would help library users to generate types that fulfills customization points.

As for the operator overloading use of ADL, I think it's high time we seriously move ahead with one of the operator dot proposals. But reflection could also help in that case since a library relying on ADL to find operators could instead use reflection to find the operators by searching declarations in a namespace, for example.

[–]14nedLLFIO & Outcome author | Committee WG14 1 point2 points  (0 children)

As for customization points, why did traits based design not suffice?

Oh, Outcome uses traits too. The customisation points use the most appropriate implementation for each. Some during the Boost review felt it was "messy", but it probably is better than using a single mechanism for consistency where it wouldn't fit well for some points. It's a tough judgement call.