Having a function only accept non-const lvalues
Asked Answered
G

5

5

I have a function which sorts two vectors with the first of them as ordering criterion. Its signature is

template<typename A, typename B>
void sort(A&& X, B&& Y)
{
  ..
}

The problem is that universal references would allow nonsense cases like

sort(vector<int>{ 2,1,3 }, vector<int>{ 3,1,2 });

where an rvalue will be destroyed afterwards (nonsense).

Asking explicitly for a lvalue doesn't work since

template<typename A, typename B>
void sort(A& X, B& Y) ... // (*)

sort(vector<int>{2,1,3}, vector<int>{3,1,2});

for some reason the above compiles (I thought only const lvalues were allowed to bind to rvalues and to prolong their lifetime?).

If I add const to the lvalue reference then the function will no longer be able to modify the vectors and sort them.


My questions are:

1) Why in the example marked with // (*) can I bind a rvalue to a lvalue that is not even const ? Why instead something like int& r = 20; isn't allowed? What's the difference?

2) How can I solve my issue i.e. having the function accept only lvalues and not rvalue temporaries? (If it's possible, of course)

Obviously I'm allowed to use any C++ version available

Geoffreygeoffry answered 22/9, 2015 at 15:20 Comment(6)
I suspect Visual Studio compiler...Injured
As @Injured alludes, the Visual Studio compiler has supported binding temporaries to non-const references for a while (unforntunately).Buyers
However it does raise warnings for it at least if you use a high enough warning level - which you probably shouldJocelin
Specifically, MSVC 2013 will give a warning of "mysourcefile.cpp(10): warning C4239: nonstandard extension used : 'argument' : conversion from '<type1>' to '<type2>' 1> A non-const reference may only be bound to an lvalue" only on warning level /W4 or above.Alainealair
@Alainealair good informationInjured
Yes, MSVC 2015 community edition :(Geoffreygeoffry
I
8

The answer is: your compiler is wrong.

Check on gcc or clang or similar and you'll get something like this:

prog.cpp: In function 'int main()': prog.cpp:9:45: error: invalid initialization of non-const reference of type 'std::vector&' from an rvalue of type 'std::vector' sort(vector{2,1,3}, vector{3,1,2}); ^ prog.cpp:6:6: note: initializing argument 1 of 'void sort(A&, B&) [with A = std::vector; B = std::vector]' void sort(A& X, B& Y) { }

Injured answered 22/9, 2015 at 15:26 Comment(2)
Why isn't the compiler deducing the type A = const vector<int>? I mean I can explicitly call split<const vector<int>, const vector<int> >(vector<int>{2,1,3}, vector<int>{3,1,2}) and the compiler will happily accept my temporaries. The only far reference to this I could find is: " If P is a cv-qualified type, the top-level cv-qualifiers are ignored for deduction." from en.cppreference.com/w/cpp/language/template_argument_deductionGi
@Gi because for an rvalue a const reference is not an exact match for template deduction. You can define const vector<int> a{2, 1, 3}, b{3, 1, 2}; then a, b are lvalues and thus const reference will be an exact match or instantiate the template yourself to bind the rvalue to a const reference. Compare https://mcmap.net/q/1428127/-why-aren-39-t-template-type-parameters-inferred-as-39-const-39-duplicate and https://mcmap.net/q/1093819/-deducing-references-to-const-from-rvalue-argumentsInjured
B
2

You can use the /Za compiler option to turn this into an error:

error C2664: 'void sort<std::vector<int,std::allocator<_Ty>>,std::vector<_Ty,std::allocator<_Ty>>>(A &,B &)' : cannot convert argument 1
from 'std::vector<int,std::allocator<_Ty>>' to 'std::vector<int,std::allocator<_Ty>> &'
        with
        [
            _Ty=int
,            A=std::vector<int,std::allocator<int>>
,            B=std::vector<int,std::allocator<int>>
        ]
        and
        [
            _Ty=int
        ]
        and
        [
            _Ty=int
        ]
        A non-const reference may only be bound to an lvalue

Note that /Za has had quite some issues in the past and even nowadays still breaks <windows.h>, so you cannot use it for all compilation units anyway. In a 2012 posting titled "MSVC /Za considered harmful", Microsoft senior engineer Stephan T. Lavavej even recommends not using the flag, but you should also have a look at the comments at STL Fixes In VS 2015, Part 2, where he says:

We've definitely had meetings about the /Za and /Zc conformance options. We ultimately want to get to a point where VC is conformant by default, without having to request extra options, so that becomes the most-used and most-tested path. As you can see in the post, I've been working towards this in the STL by removing non-Standard machinery whenever possible.

So, chances are this will be a compilation error by default in some future version of MSVC.


One other thing: The C++ standard does not distinguish between errors and warnings, it only talks about "diagnostic messages". Which means that MSVC actually is conforming as soon it produces a warning.

Bartram answered 22/9, 2015 at 16:33 Comment(5)
Wish STL would stop calling it the STLCarmelcarmela
@LightnessRacesinOrbit: I know, I know... on the other hand, with a name like this, I'd say he shall be forgiven.Bartram
@LightnessRacesinOrbit it seems Herb and Bjarne are officially naming standard library as STL (see cppcore repository). So I suppose from now on STL = STandard LibraryGeoffreygeoffry
@Dean: Neither Herb nor Bjarne have the authority to define an official naming. Only what's in the standard ISO document can be considered official.Bartram
@ChristianHackl: I'd counter that it's even worse coming from someone with the same name :PCarmelcarmela
E
1

As noted by other answers, the compiler is wrong.

Without having to change compiler of compiler options:

struct sfinae_helper {};
template<bool b>
using sfinae = typename std::enable_if<b, sfinae_helper>::type*;
// sfinae_helper, because standard is dumb: void*s are illegal here

template<class A, class B,
  sfinae<!std::is_const<A>::value&&!std::is_const<B>::value> = nullptr
>
void sort(A& X, B& Y) ... // (*)

sort(vector<int>{2,1,3}, vector<int>{3,1,2});

will fail to compile in MSVC2013 as well, and should be compliant in compliant compilers.

Note that while deducing A and B as const X is not legal under the standard, explicitly passing const X as A or B is.

A final approach is:

template<typename A, typename B>
void sort(A& X, B& Y) ... // (*)
template<typename A, typename B>
void sort(A&& X, B&& Y) = delete;

where we generate an explicitly deleted one that should be preferred to the A&, B& one. I do not know if MSVC properly picks the perfectly forwarded one in that case, but I hope so.

Elgon answered 22/9, 2015 at 17:41 Comment(3)
The sfinae_helper example compiles without errors and warnings in MSVC 2013 (unless one uses /W4 or /Za).Bartram
Yes, there is no error at all. Unless I messed something up when I turned it into a full piece of code (i.e. putting call into main, adding <vector> and <type_traits> includes).Bartram
@chris I wonder if the "bind rvalues to lvalues" is happening, and I misunderstood it as a type deduction problem.Elgon
M
1

As an answer to the X problem you're trying to solve rather than the Y problem you asked... the right answer is that you shouldn't do what you're trying to do. Being unable to imagine how something can be useful is not an adequate reason to go out of your way to prevent people from being able to do it.

And, in fact, I don't even have to suggest this in the abstract: here are two concrete examples where accepting a temporary object would be useful.

You might only care about one of the two objects:

interesting_container A;
// fill A
sort(an_ordering_criterion(), A);

The containers aren't 'self-contained'; e.g. a container that provides a view into another one:

vector<int> A, B;
// fill A and B
sort(make_subsequence(A, 1, 10), make_subsequence(B, 5, 14));
Mexicali answered 23/9, 2015 at 6:46 Comment(0)
S
0

You can explicitly delete undesired overloadings of sort function:

#include <iostream>
#include <vector>

#include <cstdlib>

template< typename X, typename Y >
void
sort(X &, Y &)
{
    static_assert(!std::is_const< X >{});
    static_assert(!std::is_const< Y >{});
}

template< typename X, typename Y >
int
sort(X const &, Y &) = delete;

template< typename X, typename Y >
int
sort(X &, Y const &) = delete;

template< typename X, typename Y >
int
sort(X const &, Y const &) = delete;

int
main()
{
    std::vector< int > v{1, 3, 5};
    std::vector< int > const c{2, 4, 6};
    ::sort(v, v); // valid
    { // has been explicitly deleted
        //::sort(v, c); 
        //::sort(c, v);
        //::sort(c, c);
    }
    { // not viable: expects an l-value for 1st argument
        //::sort(std::move(v), v); 
        //::sort(std::move(v), c);
        //::sort(std::move(c), v);
        //::sort(std::move(c), c);
    }
    { // not viable: expects an l-value for 2nd argument
        //::sort(v, std::move(v));
        //::sort(v, std::move(c));
        //::sort(c, std::move(v));
        //::sort(c, std::move(c));
    }
    { // not viable: expects an l-value for 1st or 2nd argument
        //::sort(std::move(v), std::move(v));
        //::sort(std::move(v), std::move(c));
        //::sort(std::move(c), std::move(v));
        //::sort(std::move(c), std::move(c));
    }
    return EXIT_SUCCESS;
}
Silvie answered 23/9, 2015 at 6:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.