39

We consider the goal of creating two different types, using the exact same syntax. This can be easily done with lambdas:

auto x = []{};
auto y = []{};
static_assert(!std::is_same_v<decltype(x), decltype(y)>);

But instead of using lambdas, we are looking for another, more elegant syntax. Here are some tests. We start by defining some tools:

#include <iostream>
#include <type_traits>
#define macro object<decltype([]{})>
#define singleton object<decltype([]{})>

constexpr auto function() noexcept
{
    return []{};
}

template <class T = decltype([]{})>
constexpr auto defaulted(T arg = {}) noexcept
{
    return arg;
}

template <class T = decltype([]{})>
struct object
{
    constexpr object() noexcept {}
};

template <class T>
struct ctad
{
    template <class... Args>
    constexpr ctad(const Args&...) noexcept {}
};

template <class... Args>
ctad(const Args&...) -> ctad<decltype([]{})>;

and the following variables:

// Lambdas
constexpr auto x0 = []{};
constexpr auto y0 = []{};
constexpr bool ok0 = !std::is_same_v<decltype(x0), decltype(y0)>;

// Function
constexpr auto x1 = function();
constexpr auto y1 = function();
constexpr bool ok1 = !std::is_same_v<decltype(x1), decltype(y1)>;

// Defaulted
constexpr auto x2 = defaulted();
constexpr auto y2 = defaulted();
constexpr bool ok2 = !std::is_same_v<decltype(x2), decltype(y2)>;

// Object
constexpr auto x3 = object();
constexpr auto y3 = object();
constexpr bool ok3 = !std::is_same_v<decltype(x3), decltype(y3)>;

// Ctad
constexpr auto x4 = ctad();
constexpr auto y4 = ctad();
constexpr bool ok4 = !std::is_same_v<decltype(x4), decltype(y4)>;

// Macro
constexpr auto x5 = macro();
constexpr auto y5 = macro();
constexpr bool ok5 = !std::is_same_v<decltype(x5), decltype(y5)>;

// Singleton
constexpr singleton x6;
constexpr singleton y6;
constexpr bool ok6 = !std::is_same_v<decltype(x6), decltype(y6)>;

and the following test:

int main(int argc, char* argv[])
{
    // Assertions
    static_assert(ok0); // lambdas
    //static_assert(ok1); // function
    static_assert(ok2); // defaulted function
    static_assert(ok3); // defaulted class
    //static_assert(ok4); // CTAD
    static_assert(ok5); // macro
    static_assert(ok6); // singleton (macro also)

    // Display
    std::cout << ok1 << std::endl;
    std::cout << ok2 << std::endl;
    std::cout << ok3 << std::endl;
    std::cout << ok4 << std::endl;
    std::cout << ok5 << std::endl;
    std::cout << ok6 << std::endl;

    // Return
    return 0;
}

this is compiled with the current trunk version of GCC, with options -std=c++2a. See result here in compiler explorer.


The fact that ok0, ok5 and ok6 work are not really a surprise. However, the fact that ok2 and ok3 are true while ok4 is not is very surprising to me.

  • Could someone provide an explanation of the rules that make ok3 true but ok4 false?
  • Is it really how it should work, or this is a compiler bug concerning an experimental feature (lambdas in unevaluated contexts)? (reference to the standard or to C++ proposals are very welcomed)

Note: I really hope this is a feature and not a bug, but just because it makes some crazy ideas implementable

Vincent
  • 57,703
  • 61
  • 205
  • 388
  • 21
    *Tosses upvote and runs away* – Quentin Apr 02 '19 at 13:01
  • 2
    As far as I know, this is how it is intended. Default arguments are applied in the context of the caller; therefore each call site has a different lambda an thus a different type. `ctad` has no default argument. The single Landa definition is same. I'd be curious whether it is the same when the template arguments differ. – eerorika Apr 02 '19 at 13:10
  • 6
    http://www.eel.is/c++draft/temp.arg#8 almost gives you an answer for `ok2`/`ok3`, for CTAD, there is a single definition of the constructor, so a single instantiation of the `decltype([]{})`. – Holt Apr 02 '19 at 13:12
  • 1
    Hm, I see... Following @Holt's link, we effectively have `defaulted()` twice. And just as we get two different types for the two `[]{}` with `x0` and `y0`, we get two of these for `x2`/`y2`... – Aconcagua Apr 02 '19 at 13:25
  • 4
    hmmm... almost look like another way to do compile time stateful programming – Guillaume Racicot Apr 02 '19 at 13:26
  • 2
    @GuillaumeRacicot You are reading my mind... – Vincent Apr 02 '19 at 13:34
  • 1
    At first I wanted to ask how this would leave to stateful `constexpr`, then I realized, and now I wish I hadn't so I wouldn't have to think about the inevitable horrors that are sure to follow. I can feel the agony of the standard writers from over here... – Max Langhof Apr 02 '19 at 13:54
  • [For those who do not yet know how deep the `constexpr` rabbit hole goes](http://b.atch.se/posts/constexpr-counter/)... – Quentin Apr 02 '19 at 13:58
  • 5
    I believe you're in violation of http://www.eel.is/c++draft/temp.res#8.5, but I'm not sure. – Rakete1111 Apr 02 '19 at 14:15
  • @Rakete1111 Could you elaborate on which part you think is violated here and why? At least based on my understanding, default template arguments are not part of a template specialization/instantiation, so this paragraph doesn't really seem to apply here!? – Michael Kenzel Apr 02 '19 at 16:14
  • @MichaelKenzel they're part of the specialization if they are used (if you don't specify any template arguments for example)? – Rakete1111 Apr 02 '19 at 16:18
  • 3
    https://stackoverflow.com/q/54410522/9585016 – Language Lawyer Apr 02 '19 at 16:40
  • @Rakete1111 Sorry, poor choice of words on my part. The type specified by a default argument will of course be part of the name of a particular specialization that uses that default argument. But an instantiation of a template is what is generated from the template for a given argument list. An instantiation does not have parameters or default arguments. All the parameters need to be fixed before you even get to an instantiation. So at least in my reading of this paragraph, I don't see how default arguments could possibly be one of the "constructs" that could be "interpreted" differently!? – Michael Kenzel Apr 02 '19 at 17:00
  • @Rakete1111: in the case where ok3 would be ill-formed, would that also be the case of ok2? – Vincent Apr 02 '19 at 17:44
  • @MichaelKenzel oh I see what you mean. Yes an instantiation does not have default arguments and yes every argument needs to be fixed. But you can instantiate a template with no argument list. The missing template arguments are then taken from the default arguments from the templates. So they can part of the current instantiation indirectly if that makes sense. – Rakete1111 Apr 02 '19 at 18:48
  • @Vincent yes :) – Rakete1111 Apr 02 '19 at 18:49
  • My mental model is that default template arguments are instantiated at the point of instantiation, such that `foo<>` is equivalent to `foo`. I'll admit I have trouble rationalizing why CTAD behaves differently, though. https://wandbox.org/permlink/LMD2QRA6DPVX3ka3 – Louis Dionne Apr 03 '19 at 16:28
  • @LouisDionne Isn't it simply that `decltype([]{})` is not a dependent name and thus evaluated in the deduction guide definition already? – Max Langhof Apr 03 '19 at 16:33
  • @LouisDionne Well ok it's not quite that, but the CTAD situation is pretty much the last one here: https://wandbox.org/permlink/TDRgtlNMZmVlUsTa – Max Langhof Apr 03 '19 at 16:40

2 Answers2

2

Could someone provide an explanation of the rules that make ok3 true but ok4 false?

ok3 is true because uses lambdas type as default type.

The type of a lambda-expression (which is also the type of the closure object) is a unique, unnamed non-union class type,

Hence, default template type for object, template parameter type for macro and singltone always different after every instatiation. But, for function function call returned lambda is unique and its type is unique. Template function ctad has template only for parameters but return value is unique. If rewrite function as:

template <class... Args, class T =  decltype([]{})>
ctad(const Args&...) -> ctad<T>;

In this case return type will be defferent after every instantiation.

Andrey Sv
  • 247
  • 3
  • 6
0

for ok2 function parameter type (T) depends on specified template parameter. for ok3 ctor is not a template.

for ok4 both deductions depend on same parameter type list (which is empty in this case) and due to that deduction occurs only once. template instantiation and deduction are different things. while for same parameter type list deduction occurs only once, instantiation occurs for all usages.

look this code (https://godbolt.org/z/ph1Wk2). if parameters are different for deduction, seperate deductions occur.

  • 2
    Since the question is tagged `language-lawyer`, could you give some references to the standard. Maybe a quote on *"deduction occurs only once"*? – HolyBlackCat Apr 17 '19 at 23:55
  • I didn't look at the standard, but we know that a template instantiation can be occured at outside of the template definition. But deduction have to be supplied with related function and there isn't any necessity for occuring more than once. I think it isn't specified at the standard. Also in this example Args isn't dependent upon T. For given function "ctad(const Args&...)" becomes "ctad()" and due to that Args is an empty parameter pack. – mfurkanuslu Apr 29 '19 at 20:02