| ▲ | Panzerschrek a day ago |
| I don't understand people reinventing macro-based hacks in C to achieve what was achieved in other languages many years ago. Why not using C++, for example? It's available almost everywhere, introducing its usage in an existing C codebase is pretty simple. Sure, C++ has its own downsides, but is it better to create mess with macros in C rather then using exiting language facilities and standard library containers provided by C++? |
|
| ▲ | uecker a day ago | parent | next [-] |
| Having used C++ a lot in the past, I think the mess in C++ is way worse. I also contribute to projects that introduce C++ into a C code base, and I really wish they hadn't done this. Although there are C features I like that I would need to remove before introducing C++ to a codebase, so this may not always be so simple. |
| |
| ▲ | uhuaua 21 hours ago | parent | next [-] | | If C++ adds pattern matching, and it works in practical usage across projects, will the C ecosystem consider adding some kind of pattern matching, perhaps inspired by what comes into C++, perhaps not? Also, a question for a C expert like you, uecker: In your opinion, does the compound literals feature have complex lifetimes rules or complex semantic rules otherwise? | | |
| ▲ | uecker 4 hours ago | parent [-] | | Personally, I would like to have pattern matching, but I think we need some other features first. I am usually a bit scared about what C++ comes up with. I am not sure I understand the second question. I do not think compound literals have complex lifetime rules. The lifetime end with the corresponding block, as do other objects in C (one needs to know what a block is, some people confuse this with scope). There is some potential issue which may be surprising when passing them to macros that make use of the statement expression extension, as the lifetime is then limited to the macro body. |
| |
| ▲ | Panzerschrek a day ago | parent | prev [-] | | > the mess in C++ is way worse What is mess in C++? Yes, it has some shady parts and is more complex than C, but this complexity provides expressiveness and type safety. And you can always use only features from C++ you find useful. > also contribute to projects that introduce C++ into a C code base, and I really wish they hadn't done this. What is problematic in these projects other than unfamiliarity of C++ for developers previously used only C? | | |
| ▲ | _gabe_ a day ago | parent | next [-] | | > What is mess in C++? Just a couple examples: How do you make a shallow copy of an object in C++, how do you do the same thing in C? Knowing I can just memcpy _any_ struct in C and have a valid shallow copy is pretty huge. How do you create a stable ABI in C++? How do you make one in C? In C, knowing that all my exported functions will mostly just work and be stable as long as I don’t change the interface is also huge and allows you to create cross language bindings pretty trivially. | | |
| ▲ | variadix a day ago | parent | next [-] | | You can’t necessarily do that in C either, it depends on if your object is in a container that relies on pointer stability. Intrusive linked lists are common in C and they get broken by this, for example. | |
| ▲ | Panzerschrek a day ago | parent | prev | next [-] | | > How do you make a shallow copy of an object in C++ Shallow copies aren't generally possible for classes storing something indirectly, since it violates ownership semantics. That's not how things are done in C++.. Operator = is usually used for making copies, which is optimized to memcpy for POD structures, but for something more complex may perform extra work for doing an actual deep copy. > How do you create a stable ABI in C++ It's a complex topic. Basic ABI for calling functions is identical to C. But one need to keep in mind, that type layouts and internal implementations of library types (like containers) may differ from implementation to implementation and from version to version. > knowing that all my exported functions will mostly just work and be stable as long as I don’t change the interface Nothing prevents you doing this in C++. You can have C-style external interface and use all goods of C++ internally. | | |
| ▲ | 1718627440 a day ago | parent | next [-] | | Yeah, and that's the reason why people prefer to write in C. "That's not how things are done in C++" I like C, because I can just decide for myself how things are done here. | |
| ▲ | _gabe_ 16 hours ago | parent | prev | next [-] | | > Nothing prevents you doing this in C++. You left out the part of my comment where I said “pretty trivially” lol. C++ is mostly a superset of C, which is why I’m not arguing that it’s impossible to do these things in C++. I like C++ too, and think some of the things in there are great. But the simplicity of C is nice. I would often get overwhelmed thinking about all the different constructors in C++ and what I needed to do to ensure that I didn’t accidentally tank performance by copying objects around everywhere (and potentially calling some invisible constructor that did extra work in the copy function). In C, I know when I memcpy it’s just gonna move some bytes around. Overall, I really don’t care all that much one way or the other. But I can see how people could prefer C to C++. I could also see how people prefer C++. I really really miss function overloading and <string.h> when I use C | |
| ▲ | mpweiher a day ago | parent | prev | next [-] | | QED. | |
| ▲ | kunley 11 hours ago | parent | prev [-] | | You seem to be asking question "why people use hacks in C instead of coding in other languages which are more C++ like". Then you describe what is to be C++ like. Well, maybe the issue is that certain people precisely want to avoid the features you just described. Have you considered that? |
| |
| ▲ | kccqzy a day ago | parent | prev [-] | | Calling memcpy on any random old struct in C is how you get bugs. If the struct contains other pointers: who is in charge of freeing them? What if the struct has internal pointers (a pointer pointing to a subobject of the struct)? You are playing with fire and you know it. |
| |
| ▲ | lelanthran a day ago | parent | prev | next [-] | | > And you can always use only features from C++ you find useful. Ah, the old "programmers just need to be more disciplined when using C++". Yeah, C programmers have never seen that argument before... | | |
| ▲ | kccqzy a day ago | parent [-] | | As if C programmers don’t have to be disciplined. Look if you are a C programmer you need to pick good parts of the language to write in as well. A beginner picked gets()? Oh the foes of the young and inexperienced. You picked VLA? Linus would breathe fire unto you. A difference between C++ programmers and C programmers is that C++ programmers have the awareness to know their language has bad parts to avoid, but C programmers are oblivious to the bad parts until someone uses it, but otherwise sing paeans for the language. | | |
| |
| ▲ | uecker a day ago | parent | prev [-] | | My macro-vector types are type safe. In fact this is the point. I do not think C++ is more expressive or type safe. | | |
| ▲ | ranger_danger a day ago | parent [-] | | In C, assigning the return value from malloc() to a specific type merely produces a warning: $ echo -ne "#include <stdlib.h>\nint foo() { return malloc(42); }" | gcc -xc -fsyntax-only -c -
<stdin>: In function ‘foo’:
<stdin>:2:20: warning: returning ‘void *’ from a function with return type ‘int’ makes integer from pointer without a cast [-Wint-conversion]
Whereas in C++ it's a hard error: $ echo -ne "#include <stdlib.h>\nint foo() { return malloc(42); }" | gcc -xc++ -fsyntax-only -c -
<stdin>: In function ‘int foo()’:
<stdin>:2:26: error: invalid conversion from ‘void*’ to ‘int’ [-fpermissive]
Does this not count as "more type safe" to you? | | |
| ▲ | uecker 14 hours ago | parent | next [-] | | In both languages this is a constraint violation, and it is fully at the discretion of the compiler on whether it emits a warning or error (often controllable by compiler flags). For my C compiler (GCC) this is an error. | |
| ▲ | wahern 20 hours ago | parent | prev | next [-] | | What version is your compiler? As of GCC 14 and clang 15 an implicit pointer conversion to int is an error, not merely a warning; and this is without specifying any special conformance or warning modes. I think implicit int conversions have been a constraint violation since at least C99, requiring a diagnostic, though I'm not sure if the switch to an error instead of a warning was prompted by a change in wording in C23 or just a general change in attitude and less concern with failing on pre-existing (but wrong) code. | | |
| ▲ | ranger_danger 20 hours ago | parent [-] | | This was GCC 11. I did try clang 15 and got an error (whereas 14 did not), but my point was that the language spec itself allows this, regardless of any specific compiler, and the original argument (as I understood it) was about the type safety of C and C++ (the language specs) themselves. | | |
| ▲ | wahern 18 hours ago | parent [-] | | I'm not very familiar with the C++ standard, but I don't think the C++ standard requires compilation to fail, either. It has similar language to the C standard. Per C++23 4.1.1p2.3, > Otherwise, if a program contains a violation of any diagnosable rule or an occurrence of a construct described in this document as “conditionally-supported” when the implementation does not support that construct, a conforming implementation shall issue at least one diagnostic message. The C++ standard does seem to explicitly require the compiler to reject translation units with a failed static_assert. (The requirements for #error are a little confusing; not sure if a failed #error in a conditional block requires failure.) But I couldn't find a rule that mandates failure for invalid implicit int conversions. Implicit pointer to int conversions are invalid for the same reasons in both C and C++. Basically, AFAICT C++'s type safety in this regard is a matter of historical compiler behavior, and now the major C compilers are adopting that behavior, which they previously had allowed (with a mandatory diagnostic message) for backward compatibility reasons. |
|
| |
| ▲ | 1718627440 a day ago | parent | prev [-] | | No, this counts as C++ is stupid to me. The memory returned by malloc is UNTYPED. The sole reason why void as a type exists is to convey the notion of no type, so that it needs to be assigned to a type by a programmer. If you mean "byte region, please cast before use" that's 'char *'. Since C++ now forces me to write a cast, it effectively forces me to hide and silence errors. I do want an error when I violate types, I do not want it for void, because that's the whole meaning of void. C++ manages to make it the worst of both worlds. | | |
| ▲ | josephg 21 hours ago | parent [-] | | Nah. Malloc’s return type is a pointer. The memory pointed to by a malloc call is untyped. But the return type isn’t void. It’s void*, aka a pointer to unknown. I’m mostly happy with implicit casts from void* to int* or something. But in this code example, we see an implicit cast from void* to int, casting the pointer itself into a number that might not even fit the pointer. This is rarely what you want. I’d much rather explicit casts in this case. | | |
| ▲ | 1718627440 9 hours ago | parent [-] | | You're right, I failed at reading comprehension. My understanding is that C has a different understanding of the word warning than other languages. Such as in "stern warning" or "warning: the building will collapse if you take that brick out". This is a philosophical problem. C has false positives to the question: "Is that a program", and some people find that to be idiotic (I don't, this is why I like C). But this becomes really logical, when you have in mind, that the point is that you do not want to have false negatives. |
|
|
|
|
|
|
|
| ▲ | Joker_vD a day ago | parent | prev | next [-] |
| For one reason, it is because C++ runtime (and its standard library) is a whole own can of worms which most people would rather not touch if they can afford to. Which they mostly can. > Sure, C++ has its own downsides "Sure, getting your eyes gouged out has its downsides, but is it better to read that awful mess of macros in C instead?" The answer most people would give to this question may surprise you. |
| |
| ▲ | kccqzy a day ago | parent [-] | | Here we go again. Yes there are bad parts in the C++ standard library and there are good parts. But if you are just trying to do type safe generic data structures you are unlikely to touch the bad parts. Many codebases forbids parts of the standard library, e.g. LLVM forbids including <iostream>. You can forbid using parts of the standard library too. | | |
| ▲ | Joker_vD a day ago | parent | next [-] | | > Yes there are bad parts in the C++ standard library and there are good parts. Okay, other than <vector>, what are the good parts? Because as the sibling comments rightfully point out, migrating your codebase from C to C++ just to be able to use <vector> is not worth it. The <map> is a sad joke played upon the C++ programmers by the standard committee. | | |
| ▲ | pjmlp a day ago | parent | next [-] | | Strings, something that C still doesn't do properly, not even having something like SDS into the standard library. <map> does the work just fine, not everyone has winning microbenchmarks as part of their daily work. | | |
| ▲ | accelbred a day ago | parent | next [-] | | Many of the big C++ projects I've worked with have custom string types since the standard one was defficient for some reason or another. | | |
| ▲ | jstimpfle a day ago | parent | next [-] | | Exactly, and a simple usable string type is just a struct MyString { char *buf; size_t len; }; away. Actual magic is in how you use it, where you allocate it, how you integrate allocation and formatting and logging and I/O... i.e. all the things that aren't solved by crufty complex std::string either. | | |
| ▲ | pjmlp 14 hours ago | parent [-] | | So simple, and yet most C projects never do it. Plain C strings all over the place. Since I learnt C in 1990, knowing to count up to 10 is enough, for the amount of times I saw it actually being done. | | |
| ▲ | jstimpfle 13 hours ago | parent [-] | | The projects and people I follow do this all the time. Just like putting a size field near each regular non sentinel terminated array is useful. But if not doing it (maybe linux kernel in many cases), no big deal either, don't sweat the small stuff. String slices are small and rarely perf critical. |
|
| |
| ▲ | pjmlp 14 hours ago | parent | prev [-] | | And yet zillion ways better than C strings, which was the point. | | |
| |
| ▲ | Joker_vD a day ago | parent | prev [-] | | > <map> does the work just fine It has a rather weird interface, at least until C++ 17 when some of the deficiencies were patched somewhat. | | |
| ▲ | jstimpfle a day ago | parent [-] | | It's a slow generic data structure with an unintuitive API. You can use it for leetcode or for CRUD. For anything more demanding it's horrifically bloated and bad. std::string too. Whenever you see STL datatypes like even string and map, you have to deal with RAII, implicit allocations, weird operator syntax, unexpected mutation (invalidation) and so on. (Spporting or even encouraging destructive mutation, and by this I mean not incrementing counters or anything harmless but allowing iterator invalidations and crashes, are also why std::vector is bad in my opinion, these are idiomatic APIs for 90s and 2000s programming, which we should know better to avoid in 2026). | | |
| ▲ | Panzerschrek a day ago | parent | next [-] | | > For anything more demanding it's horrifically bloated and bad C++ containers are designed for average demands. If you need something more specific, you can always use an alternative implementation. And it's better than messing with macros in pure C. > you have to deal with RAII What's problematic with it? > implicit allocations Allocations aren't implicit. It's usually clear from the documentation where allocation takes place (like in concatenating strings or copying strings). > unexpected mutation (invalidation) It's not the case with standard library containers. Mutating methods aren't const-qualified, so that it's clear where mutation can take place. And in C++ it's strictly recommended to mark as const everything in regular user code which shouldn't be mutated. | | |
| ▲ | jstimpfle a day ago | parent [-] | | > C++ containers are designed for average demands. If you need something more specific, you can always use an alternative implementation. As someone who has been programming exclusively in C/C++ for a long time -- the biggest selling point for these languages in 2026 is exactly being suited to doing something specific, not generic. > Allocations aren't implicit std::map<int,int> m;
m[1] = 42; // implicit alloc
auto m2 = m; // implicit too
> in C++ it's strictly recommended to mark as const everything in regular user code which shouldn't be mutated.Theory and practice. Making everything const correct is too painful in practice, often impossible. It's a trap for bean counters that can't focus on getting some actual useful work done. I'm being harsh to my own past self here. Counter question: why include allocation mechanics with some existing data that never gets changed? Why should we have to add much more boilerplate to get the simpler thing? | | |
| ▲ | Panzerschrek a day ago | parent [-] | | > m[1] = 42; // implicit alloc operator[] of associative containers is mutating and may allocate. It's mentioned in the documentation. And there is no operator[] overloading for const instances, so that you can't trigger an allocation by just reading elements from it. > auto m2 = m; // implicit too Taking a copy requires making an allocation. Do you expect some other behavior in such case? > Making everything const correct is too painful in practice, often impossible Maybe you are dealing with some legacy codebase (from 90s)? I have worked with multiple codebases in past 10 years or so and keeping things which shouldn't be mutated const wasn't a problem at all. All the code was written using such approach. > why include allocation mechanics with some existing data that never gets changed I don't think I fully understand this question. What never gets changed? In your example you are mutating a container and taking a copy of it (which can be changed later). | | |
| ▲ | Lumich 12 hours ago | parent | next [-] | | > auto m2 = m; // implicit too
Taking a copy requires making an allocation.
C++ makes it “look nice” at the cost of clarity. But not consistently. Overall, despite addition of syntax sugar in various places to make things look smooth, we arguably have “a lot of syntax” (“syntax castles”) in C++, as a result of embracing a lot of complexity. C, OTOH, avoids both sugar and castles. | |
| ▲ | jstimpfle a day ago | parent | prev [-] | | > Do you expect some other behavior in such case? No, just saying this stuff is implicit and thus hard to read. More so the allocation on indexing assignment.
and keeping things which shouldn't be mutated const wasn't a problem at all. > and keeping things which shouldn't be mutated const wasn't a problem at all. As soon as datastructures become more complicated and more interconnected, it's not clear anymore what const should even mean (issues are similar as with deep vs shallow copies, how do you even draw the lines, where are the objects?). And the major philosophical flaw in the const vs. non-const distinction is that to make a strict separation you have to move everything mutable into constructor calls (for the most part, not getting more into the weeds of C++). Which is very awkward, it's a real tradeoff you need to be aware of. I realized only after playing these games for a long time what a waste of time it is and how much complexity it creates. | | |
| ▲ | pjmlp 14 hours ago | parent [-] | | Great that setjmp/longjmp is explicit, and that in C there are even more liberties to cast away const than in C++, while some surprises in embedded, when the compiler decides const could have been placed into read only memory. | | |
| ▲ | jstimpfle 13 hours ago | parent [-] | | Placing data in .rodata (read only memory) is an actual useful application of const keyword. Not specific to embedded. Arguably there shouldn't have been multiple slightly different meanings of the keyword like that. I don't know why you are now jumping to setjmp/longjmp, trying to make a point that somehow C is the worst programming languages ever, when all I am trying to argue is for sane programming practices, hinting that more complex features (line especially found in newer languages) are not solutions but part of the complexity problem. |
|
|
|
|
| |
| ▲ | pif a day ago | parent | prev | next [-] | | > you have to deal with RAII If you find that RAII is a problem, I pity how poor a programmer you must be... | | |
| ▲ | jstimpfle a day ago | parent | next [-] | | You must be very experienced to be so judgemental! But maybe ask the many billions of voxels that get tested per second on my multithreaded and SIMD'ed cutting simulation. | | |
| ▲ | kccqzy a day ago | parent [-] | | C++ is the best language to write multi-architecture SIMD without relying on compiler magics like autovectorization. It gives you enough tools to define zero-cost abstractions to make SIMD nice. | | |
| ▲ | jstimpfle 21 hours ago | parent [-] | | Yeah... There are libraries like Google Highway but that seems to me like a bulky software abstraction layer even if it is a so-called "zero cost" abstraction. Maybe appropriate in some cases. In my case, I've implemented a couple of lines for AVX2 and AVX512, maybe around 100 lines for the most perf sensitive sections currently, not a big deal. The actual work is in the understanding of the problem and of the phyiscal hardware, and in the software architecture and data layout to even enable vectorized stream processing in the first place. I'm not concerned writing a couple of perf sensitive lines against 2-3 most important concrete vector ISAs. I also fancy adding a GPU backend later (requires D3D12/SM6.0 API level), Highway couldn't help with that. |
|
| |
| ▲ | bits_and_bytes 7 hours ago | parent | prev [-] | | RAII is terrible for memory resources - it's ok for things like locks I guess.
Rarely do I want to couple initialization with allocation, I want bulk allocations/deallocations (and I want precise control over when that happens) and reset()-like functions so I can easily reuse memory. | | |
| ▲ | jstimpfle 5 hours ago | parent [-] | | It couples things and complicates things. Which creates friction against when refactoring a software architecture, making it harder to scale it up. Fully agree, for locks or other simple begin/end pairs it doesn't create the same kind of architectural damage. But I don't use it even there, because RAII fundamentally doesn't play well with manual cleanup code. It's kind of an all-in game, if you use some RAII there are strong forces (whenever you need to get the "nesting" right) to convert all remaining manual begin/end type code to types that define that functionality out-of-line in the destructor/destructor, leading to fragmentation. |
|
| |
| ▲ | pjmlp 14 hours ago | parent | prev [-] | | At least is there in the standard library, one doesn't need to invent one from scratch in every single project, or reach out to the tools floppy while not telling the employer due to code ownership. | | |
| ▲ | jstimpfle 10 hours ago | parent [-] | | No need to invent when you already know how it works. I could re-type all the essential stuff from std::string that I've ever used myself within 10 minutes, no problem, without looking up external sources, including copy & move semantics and whatnot. But I'm deliberately not doing it. Because I don't like this "invention". |
|
|
|
| |
| ▲ | kccqzy a day ago | parent | prev [-] | | So what? You like <vector>, and make it the only allowed include. Write all other type safe generic data structures by hand using C++ syntax. Problem solved. In fact many old codebases migrated from C already has their own implementations of strings and vectors and hash tables, so these projects can totally forbid the C++ standard library versions in favor of their own versions, and only pull in <type_traits> for easier type safe generic data structure programming. |
| |
| ▲ | accelbred a day ago | parent | prev [-] | | You cant touch C++ without bringing the object lifetime stuff in. And unlike strict aliasing, theres no flag to turn it off. | | |
| ▲ | jeffbee a day ago | parent [-] | | Object lifetime is the entire point of C++ and it solves ~100% of the emergent flaws in C programs. | | |
| ▲ | accelbred a day ago | parent [-] | | I was just dealing with this: https://bugs.gentoo.org/show_bug.cgi?id=974323. Also std::start_lifetime_at is a hack and ive seen nobody using it in all the placed where it aught to be used. If optimizing based on object lifetimes could be turned off, itd be turned off everywhere for hardening like strict aliasing is. | | |
| ▲ | jeffbee a day ago | parent [-] | | In my personal opinion, the fix[1] for that issue clearly implicates C-style habits polluting a C++ code base. No right-thinking knower of C++ initializes objects with memset! Also I believe that -Wall would have flagged that, and I know for certain that cppcoreguidelines-init-variables + cppcoreguidelines-pro-type-member-init would have flagged it. 1: https://github.com/llvm/llvm-project/commit/905a88b923433eb8... | | |
|
|
|
|
|
|
| ▲ | psyclobe a day ago | parent | prev | next [-] |
| Its rather hard to introduce c++ into legacy c projects. You basically have to decide on a subset of features to use, and then you'll have to explain to the teams why the same looking code now takes 10x more compute to build. Usually the way we do it here is we honor some interface then rewrite the subsystem in c++. And then there's the real hurdle and that is getting the c++ idiom correct as it is very easy to just open the floodgates and let everyone write code that looks vastly different. |
|
| ▲ | rwbt a day ago | parent | prev | next [-] |
| I'd rather not migrate my C codebase to C++ just to use an array container. Very hard to consistently limit the codebase to a strict subset of C++. |
| |
| ▲ | pjmlp a day ago | parent [-] | | It is called a linter, more devs should learn to use a tool that was originally created for C in 1979. | | |
| ▲ | accelbred a day ago | parent [-] | | The only decent linters I know of for C++ are clang-tidy and coverity and they are not good enough. | | |
| ▲ | pjmlp 14 hours ago | parent [-] | | If the goal is language subsetting, they do the job just fine. | | |
| ▲ | rwbt 10 hours ago | parent [-] | | Can clang tidy restrict RAII entirely? Or any other linter? |
|
|
|
|
|
| ▲ | 0xbadcafebee a day ago | parent | prev | next [-] |
| If you want better tires on your car, why not just get a different car? |
|
| ▲ | tliltocatl a day ago | parent | prev | next [-] |
| Not sure why author does it, but I use C over C++ for one reason: I'd rather not have random hidden malloc() scattered in my code. And no, it's not about latency or performance. Once my code takes an yet unexplored part and runs out of 200kB RAM, the will be no way for me to learn about it - running out of memory will bring down both the logger and the radio link, which is the only way to get the logs from a sensor installed twenty meters above ground in a hazardous environment facility. C may be unsafe, but most errors short of "writing to a random memory area" are either well-contained or at least reproducible. Dynamic memory allocation means that anything may crash everything. Manageable if you have a MMU and can contain crashes within isolated heap, but if not - I'd rather not bother. And yea, you can write zero-allocation C++ code, but why bother if most of STL is now unusable? And you get some new and exciting UB modes (seriously, no union aliasing, what the heck?) Also, template metaprogramming is way more arcane than C macros. And I need compile-time metaprogramming in leu of dynamic allocation. So C macros it is for now (Zig is promising but still not there yet). |
|
| ▲ | dzhar11 20 hours ago | parent | prev | next [-] |
| (C++) The worst programming language of all time
https://www.youtube.com/watch?v=7fGB-hjc2Gc (10m ago) These macro tricks (from the article) are not about recreating C++ in C.
C is less opinionated (comp. to C++): it gives you fewer built-in abstractions; does not prescribe which DS dev. should use. It is bring-your-own-data-structures language. Bring whatever you choose.
In C, newer language standard is not always better. Older lang standards support more platforms. There is a certain beauty in writing in an old language while still applying modern coding practices. I like C. I don't hate other languages. I wouldn't use C for apps that should have been written in C# with WinForms... |
|
| ▲ | coolThingsFirst a day ago | parent | prev | next [-] |
| STL is amazing and the people that designed it were very smart. Problem is C++ has many features which I likely won't be able to understand in this lifetime. But using C-style C++ is nuts. Just the extra of STL is basically the C that C should have been. It's a shame not to have a string type or use brittle macros. |
|
| ▲ | a day ago | parent | prev [-] |
| [deleted] |