Why is returning a const from a function not being detected as a const? [duplicate]
Asked Answered
G

2

5

I have a program which depends on the result of std::is_same_v <const value_t, decltype(value)>. However, I have found that when functions are passed to this expression the result is unexpected, causing me bugs.

I thought that functions returning const value_t were going to be treated as being the same as const value_t, but this seems not to be the case since std::is_same_v <value_t, decltype(func())> is what returns true.

I tried returning this values using std::as_const, using static_cast, returning it from a constexpr function, but none of them worked as expected.

A minimal, reproducible example:

#include <type_traits>
#include <iostream>
   
inline const int x = 1;
/// A constant integer variable.
   
inline const int y() {return x;}
/// A constant integer function returning <x>.
   
int main()
{
    /// <x> successfully passes as being a constant.
    std::cout << std::is_same_v <const int, decltype(x)> << " ";
   
    /// But returning it from a function (<y>) does not.
    std::cout << std::is_same_v <const int, decltype(y())> << std::endl;
}                     

Why is this the case? How can I ensure that std::is_same_v <const value_t, decltype(value)> and std::is_same_v <const value_t, decltype(func())> both return true?

Gin answered 8/5, 2022 at 18:43 Comment(10)
Because a returned const int can be assigned to an int. int r = y(); even with auto r = y(); r is an int.Astragalus
decltype of a function like int f(int) is int(int) that is the type of the function.Buhler
@Astragalus How can I ensure that std::is_same_v <const value_t, decltype(value)> and std::is_same_v <const value_t, decltype(func())> both return true?Gin
Returning a const by value is usually pointless (and ignored). If you make that const int& instead and check is_same you would see that it's is const int&Bersagliere
@TedLyngmo I know that it is usually the case, nevertheless, that is not the point of the question. Using const int should only be seen as a specification that means that I want my function to be treated as it is returning a constant value.Gin
@Gin It sounds like constexpr/consteval would be a better fit in that case.Bersagliere
Return it in a structure:- struct cx{ const int x; cx(const int x): x(x){ } }; inline cx y() {return x;}Astragalus
@Gin const on the return type does not indicate that the function is pure or constant or anything like that. There is currently no way to indicate these properties in the standard language properly. Compilers typically have attributes as extensions to convey these properties. The closest standard feature would probably be the consteval qualifier, which indicates that the function must always return the same value for same inputs and must be computed at compile-time.Urias
Also keep in mind that you can not return move-only (i.e. non- copyable) types from a function that has a const qualified return type. An example is std::unique_ptr. That type of functions never let the move ctor to be called.Alliterate
@digito_evo: Sure you can, since C++17 where the prvalue can be used to initialize anything regardless of cv-qualification.Shinar
U
7

y() is a prvalue expression. The type of this expression is not const int, but int.

This is because the type of prvalue non-class non-array expressions have their cv-qualifiers stripped.

In other words, it will work if you use a class type, but not with non-class types.

This is just how the language works. There is no difference between a const int and a int prvalue and similar for other fundamental types. They are just values for which the qualifiers aren't useful.

In contrast the expression x is a lvalue, not a prvalue, and therefore the cv-qualifier is not stripped. There is a difference between referring to an object via const-qualified and non-const-qualified lvalue.

But even then decltype applied directly to an non-parenthesized name doesn't actually consider the type of the expression anyway, but instead considers the type of the declaration of the named entity. This is a special case. decltype((x)) would consider the expression type and yield const int&, adding the lvalue reference because x is a lvalue.

std::invoke_result is also specified to return the decltype of the INVOKE expression, so it will have the same issue.


You can obtain the const-qualified type from the return type of the function type. A typical approach to that is partial specialization based on the function type. Unfortunately doing this properly is quite cumbersome since a lot of specializations must be written out to cover all cases. It also won't work if y is overloaded or a generic lambda/functor.

My guess would be that e.g. boost::callable_traits::return_type is implemented that way and will yield the const-qualified int.

It produces the expected result (see https://godbolt.org/z/7fYn4q9vs):

#include <type_traits>
#include <iostream>
#include <boost/callable_traits/return_type.hpp>
   
inline const int x = 1;
/// A constant integer variable.
   
inline const int y() {return x;}
/// A constant integer function returning <x>.
   
int main()
{
    /// <x> successfully passes as being a constant.
    std::cout << std::is_same_v <const int, decltype(x)> << " ";
   
    /// And returning it from a function (<y>) now does as well.
    std::cout << std::is_same_v <const int, boost::callable_traits::return_type_t<decltype(y)>> << std::endl;
}
Urias answered 8/5, 2022 at 18:51 Comment(6)
For those interested: gcc.godbolt.org/z/7bvaddoPdWorms
Is there any way to ensure that the returned value of the function is a declared constant variable?Gin
@Gin I am not sure what you mean. The declared return type of the function is already const int. The return value itself is not a variable and is not something that is declared at all. As I mentioned, the language does not distinguish const int and int prvalues. They are the same thing.Urias
@Urias Hmm, my bad, sorry. So the conclusion is that there is no way to know whether a function is returning a const variable vs a non-const variable at all, right? If I wanted to ensure this, the only possible way is by using functions with the const specifier and then using callable_traits::return_type_t?Gin
@Gin Functions don't return variables. If a function doesn't return a reference, then it simply returns a value. That value could for example be 5. Now you are asking how to determine whether this value 5 is const or not. It doesn't really make sense. I think there is some context missing on what you are really trying to achieve. As others have said as well, there is no way that whether or not the return type is const int or int could ever matter to you. You can inspect the declared return type of the function with callable_traits::return_type_t but that's itUrias
@Urias Crystal clear now, thank you so much for your help.Gin
F
2

This is because of expr#6 which states:

If a prvalue initially has the type cv T, where T is a cv-unqualified non-class, non-array type, the type of the expression is adjusted to T prior to any further analysis.

This means that in your particular example, since the call expression y() is a prvalue of type const int and so the above quote applies to it and thus it will be adjusted to int before further analysis since int is a built in type and not a class type and hence the result is false in this case.


On the other hand decltype(x) gives the declared type of x which is const int and not int and hence the result is true in this case.

Note that even if you were to write decltype((x)) then also the result will be true because when x is used as an expression it is an lvalue and so the above quote does not apply to it(and no adjustment is made to const int).

Flabby answered 9/5, 2022 at 8:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.