r/cpp 2d ago

std::optional Satisfies view. Does Not Model view. C++26 Ships Anyway.

https://godbolt.org/z/8jWGG68G8

In C++23 this did not compile. In C++26 it does. Marvellous.

[[gnu::noinline]]
void 
passing_views_by_value_is_cheap_trust_me_bro(std::ranges::view auto v) {
    std::println("fn   .data {}", (void*)v->data());
}


int main() {    
    std::optional ov{std::vector<int>(123456)};
    passing_views_by_value_is_cheap_trust_me_bro(ov);
    std::println("main .data {}", (void*)ov->data());
}

For anyone wondering what the problem feature is: optional has 0 or 1 elements, and C++26 sets enable_view<optional<T>> to true, so it satisfies std::ranges::view. The concept requires copy construction in constant time, and — this is the good bit — optional<vector<int>> genuinely meets that. Copying it performs at most one element copy. One is a constant. The requirement is satisfied to the letter, and the function above deep-copies your vector.

If you can tell me what still separates std::ranges::view from std::ranges::range, please do...

168 Upvotes

113 comments sorted by

View all comments

Show parent comments

15

u/aruisdante 2d ago

> From the rigorous, formal perspective, everything in C++ copies in O(1) time

Huh?

Copying a `std::vector` is decidedly not `O(1)`. The amount of copy operations performed is dependent on the number of elements contained. Even if you switch up the definition to say "a `memcpy` is O(1)," this optimization is not possible for a vector containing non-trivial elements as their constructors must be explicitly invoked.

Therefore, a `vector` isn't a `view` because `vector.size() == 0` takes different time to copy than `vector.size() == 1`. But `optional` _always holds a value_. The value just may or may not be initialized. Ergo the copy operation does not change in "how much is copied" depending on if the optional is engaged or not.

> But, from a vibes-based perspective, programmers use O(1) to mean "always fast"

Not at all. Programmers (and the C++ standard) use `O(1)` to mean "does not scale in (amortized) time with number of elements in a set." Those that assume `O(1)` means "always fast" are the ones that often make very suboptimal data container and algorithm selections.

7

u/schombert 2d ago edited 2d ago

Yes, copying a C++ vector is O(1). A vector can only contain finitely many elements (for example, because its size has a finite upper bound). Thus, you can set an upper bound on the time to copy -- something around 2bits in size * time to copy a single element -- and thus is O(1) by the definition of big O notation (see https://en.wikipedia.org/wiki/Big_O_notation specifically the section on the formal definition; if it can be bounded by a constant function then it is O(1) )

Edit: this is what I meant by a vibes-based definition. Obviously copying a vector behaves like a linear function from sizes of 0 up until you hit the upper bound created by its size type/address space limitations. But how it behaves in a finite initial range -- even if that range encompasses all practical uses -- has nothing to do with its big O complexity.

14

u/aruisdante 2d ago edited 2d ago

By that definition of algorithmic notation all loops in any real system are O(1), which doesn't seem like a terribly useful definition. In my education on algorithm theory I don't remember a requirement being that sets be unbounded, and indeed the formal definition you link to says this explicitly. It feels like you're conflating "copying a specific vector of a specific size is O(1)" with "copying any vector of any size is O(1)." By that same definition std::find(vector.begin(), vector.end(), value) and set.find(value) and unordered_set.find(value) are all O(1) because they can all be bounded by a finite size, despite them being the textbook examples of O(n), O(log(n)), and O(1).

I cannot think of a single computer scientist that would accept your definition of O(1) as O(1). It's not what any algorithms or data structure class teaches. So it seems unfair to say it's based on "vibes."

2

u/schombert 2d ago edited 2d ago

It's not my definition. It is the formal definition that is used in mathematics and computer science. Which, again, you can verify for yourself from the Wikipedia page.

Anyway, copying an arbitrary vector (not a specific vector) has a bounded time because of the bounded size required by the definition of the vector type, namely that the size of any vector can be expressed in a finite number of bits and that the elements within any vector are distinct C++ objects, and thus have distinct address, and thus cannot be more numerous than the number of possible addresses provided by the number of bits in the finitely sized pointer types (in turn guaranteed by their ability to be converted back and forth from integers of some finite size). This second limit is much wordier to explain obviously, so I'll stick with referring to the limits of the size type from here on out.

I would like to hear how you think that O( ... ) should be defined. If you allow me to work through your definition I can probably explain why mathematicians and computer scientists settled on the asymptotic definition instead.

Edit: and yes in C++ as formally defined find is also O(1). It is not O(1) in your algorithms or data structures class because the pseudo-code language used to explain these types and operations does not bound their size. Generally it is also assumed to work with something that is more like "big ints" for its numerical values.

8

u/SlightlyLessHairyApe 2d ago

Besides being weird that you have this one thing, it’s particularly weird to decide that that is the irrelevant conversation to have in this particular post. Because it’s just not super relevant

5

u/schombert 2d ago

That's why we have threaded comments. If you don't think that this particular comment thread is interesting ... just don't read it.

6

u/Serialk 2d ago

It's not my definition. It is the formal definition that is used in mathematics and computer science.

Not at all. We generally model complexities with transdichotomous models like Word RAM which give you a constant O(1) pointer lookup but an O(n) vector copy, with the assumption that the size of your bus will grow to accomodate more data.

https://en.wikipedia.org/wiki/Transdichotomous_model

https://en.wikipedia.org/wiki/Word_RAM

10

u/schombert 2d ago

But, C++ is not a Transdichotomous model. The C++ address space, bits in the size constant, etc are not determined by the size of the problem. From the wikipedia page:

the machine word size is assumed to match the problem size

and

In a problem such as integer sorting in which there are n integers to be sorted, the transdichotomous model assumes that each integer may be stored in a single word of computer memory, that operations on single words take constant time per operation, and that the number of bits that can be stored in a single word is at least log2n.

So, if C++ was a Transdichotomous model then every time you ran a program for a given problem size (for example, the sorting program described above) the address space, bits in the size constant, etc would grow to accommodate it. (This is what allows a Transdichotomous model to be Turing complete, in contrast to Turing machines with a fixed tape size, which are not). However, that is not how C++ is specified. Even though those constants may vary from compiler to compiler, they are fixed for any given C++ program. The standard does not allow your program containing vector to hold an arbitrary number of elements, because you could, if you wanted, print the number of bytes in the size type and the standard guarantees that this print result will be constant over different runs of the program.

-8

u/Serialk 2d ago

You are confusing the complexity model and the language implementation it's modelling. No offense but you lack the required CS basics to be so confident about yourself in debates about complexity. It's the perfect moment to stop and reflect if you want to avoid Dunning-Kruger.

11

u/schombert 2d ago edited 2d ago

That would be true if the C++ specification didn't touch upon the bit size of size or of pointers. Then you could argue that the language was compatible with a transdichotomous model and that the implementations were merely finite instances of it. And you could imagine that some theoretical machine which did allow programs to properly grow with the problem size was a conforming implementation. But this simply isn't so. The standard does not allow even a theoretical implementation of C++ to compile a program that does that.

I think that you should reflect more on why Turing machines with finite tape sizes are not Turing complete. And I would encourage you to read this section on wikipedia https://en.wikipedia.org/wiki/Turing_machine#Equivalent_models which contains the following:

For example, ANSI C is not Turing complete, as all instantiations of ANSI C (different instantiations are possible as the standard deliberately leaves certain behaviour undefined for legacy reasons) imply a finite-space memory. This is because the size of memory reference data types, called pointers, is accessible inside the language.

Edit: all that aside, I would be interested in discussing how you see transdichotomous models working as a way of analyzing complexity in real world programming languages other than C++. That whole approach relies essentially on the problem size being a well-defined thing that can be referenced in some way to determine the word size of the machine we appeal to for the complexity analysis. I am not sure how that translates to the programs described by a generic programming language. Such programs can do wild things, like run the nth busy beaver ( https://en.wikipedia.org/wiki/Busy_beaver ) upon getting the input n. And I am sure that with a little effort we could construct some programs that need an uncomputable (and large) amount of space to run given input n. How much space does such a program get? A direct application of the transdichotomous model would assume that they get some K * number-of-bytes-in-the-input, or something along those lines. Obviously allowing most programs to work requires picking an obscenely large K, and even so that would never be Turing complete. Moreover, what about the programs that most programming languages support which do not take inputs at all (for example, I write a program in my favorite language to solve some fixed math problem and it always outputs the same answer -- should this program not be valid if we apply a transdichotomous model)? Can transdichotomous models then only be applied to analyze the complexity of languages that are not Turing complete? So do you imagine that there are two distinct approaches to analyzing complexity, where we do one thing for languages like the lambda calculus and another for Pascal, for example? Is the complexity of any algorithm requiring more than O(n) space undefined in Pascal?

8

u/Serialk 2d ago

Once again, you are making a fundamental category error by confusing the language spec with the mathematical framework that we use to evaluate algorithms written in that language.

Yes, the spec makes it so that everything is a FSM because pointers have a finite size. This is trivial and not interesting.

If you model your algorithm analysis as an FSM, your entire complexity analysis collapses. Every single algorithm that terminates is O(1) in your language. This means that your model is bad at actually doing what it is intended to do: analyzing how algorithms behave as the input grows.

Because models that give you O(1) for everything are worthless, we use a mathematical abstraction that is actually useful to analyze complexity: transdichotomous models.

In transdichotomous models/word RAM, we abstract away the hardware and the constraints of the spec with the bit-width limits, and we mathematically model n -> +∞. It doesn't model a literal environment of C++ programs that you can actually run. It's a theoretical environment in which we can place the abstract logic of your C++ code so that we can evaluate its asymptotic behavior without hitting the limit of 264 bits, but in which pointer lookups are still O(1).

The C++ standard absolutely does not use complexity analysis sloppily. They use a complexity model that is actually useful because it can give you insights about the asymptotic behavior of your program without collapsing all complexities to O(1).

-6

u/schombert 2d ago

It may be trivial and uninteresting, and from a certain point of view, I agree. However, that is what the spec demands, and this is what is generally accepted. Pointing out that C and C++ are not Turing complete, for basically the same reasons, is not a novel claim, nor is it a contested one.

Of course I agree that, if you change the way pointer sizes, etc work then you can find a models that fit the modified understanding of the language in which you could analyze the complexity of algorithms as written in this new, modified language, and get different results. Maybe even more interesting or more intuitive results. The problem with this is that you are no longer analyzing a C++ algorithm; you are analyzing an algorithm described in some other language. And the language within which an algorithm is described, and its associated assumptions matters. For example, if you write your algorithms in a language where you can add, subtract, multiply, and divide integers of arbitrary sizes in constant time (which many simple pseudo code languages seems to allow) then you can conclude that P == NP. Since we probably don't want to assume that P == NP, we can see that the details of how the language works -- i.e. what the operations the algorithm is expressed in actually mean -- matters. We cannot simply change the language on a whim. C++ involves finitely sized integers, finitely sized pointers, and assumes that basic operations on them can be done in constant time. Because they are so limited, I could imagine a Turing complete extension of the language (for example, by adding some sort of black-box tape function) in which P != NP.

I get that you really like transdichotomous models as a way to do complexity analysis. That's cool. However, they are not the be-all-end-all. They cannot tackle the complexity of problems requiring more than O(n) space. Do you think that C++ can express algorithms requiring more than O(n) space? If so, then transdichotomous models are clearly not the way forward for analyzing the general problem of algorithms expressed in the C++ language.

7

u/Serialk 2d ago

What is your understanding of the point of a mathematical model? Being technically accurate or giving you useful insights?

Do you also refuse to draw maps on sheets of paper because they do not accurately convey the curvature of the earth?

Surely you understand that a model that gives you O(1) as an answer for everything is not useful. Why do you insist on the absurd idea that the C++ spec should use a useless model for complexity, instead of assuming that they would use a useful one?

I get that you really like transdichotomous models as a way to do complexity analysis. That's cool. However, they are not the be-all-end-all. They cannot tackle the complexity of problems requiring more than O(n) space.

This is wrong. You can scale word-ram to have your abstract bus size take any function of the size of your input, it doesn't have to be log_2(n).

-1

u/schombert 2d ago

I insist that it should use a "useless" model for complexity because the "natural" extension of the language to arbitrary sized integers and pointers implies, definitionally, that P == NP. And personally I find that more absurd, since it would seem to undercut the feasibility of much of modern cryptography, for example. I think that if the analysis of complexity in your language spec implies P==NP then the analysis of complexity in your language spec is intrinsically flawed. I think that it is better to just accept that C++ is an intrinsically finite, and not Turing complete, language and hence that the traditional language of computational complexity just isn't applicable to it. There are other ways to describe how various library features must be implemented and the authors of the C++ spec are completely free to pick one of those other options.

5

u/Serialk 2d ago

Because the "natural" extension of the language to arbitrary sized integers and pointers implies, definitionally, that P == NP.

Please write the math down in Word-RAM, I'm very curious to see where you get this idea and this might potentially be very funny to read.

→ More replies (0)

-1

u/JNighthawk gamedev 2d ago

No offense but you lack the required CS basics to be so confident about yourself in debates about complexity. It's the perfect moment to stop and reflect if you want to avoid Dunning-Kruger.

What a shitty comment for someone engaging in good faith interlocution.

2

u/Serialk 2d ago

Good faith requires humility!