<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[PVS-Studio - Medium]]></title>
        <description><![CDATA[PVS-Studio is a tool for detecting bugs and security weaknesses in the source code of programs, written in C, C++, C# and Java. It works under 64-bit systems in Windows, Linux and macOS environments, and can analyze source code intended for 32-bit, 64-bit and embedded ARM. - Medium]]></description>
        <link>https://medium.com/pvs-studio?source=rss----9aaa9654e64---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>PVS-Studio - Medium</title>
            <link>https://medium.com/pvs-studio?source=rss----9aaa9654e64---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Fri, 13 Nov 2026 17:14:15 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/pvs-studio" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[Terrible tips for a C++ developer: 2026 edition, Part 1]]></title>
            <link>https://medium.com/pvs-studio/terrible-tips-for-a-c-developer-2026-edition-part-1-7be5db120bd5?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/7be5db120bd5</guid>
            <category><![CDATA[cpp-programming]]></category>
            <category><![CDATA[learning]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[cpp]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Fri, 13 Nov 2026 13:54:06 GMT</pubDate>
            <atom:updated>2026-11-13T13:54:06.833Z</atom:updated>
            <content:encoded><![CDATA[<p>We’re introducing an updated edition of the terrible tips for a C++ developer book. We’ll be posting it on our blog in parts. The book includes a total of 60 tips.</p><p>Each part will feature a list of “tips” that developers really shouldn’t follow. Each tip comes with an explanation of why it’s bad, as well as a breakdown of its less obvious aspects. This series is designed to help beginner programmers learn the ropes, while entertaining experienced developers with some of the more unexpected C++ quirks.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*EbY8DgvohJa5O5dr.png" /></figure><h3>Foreword</h3><p>The first edition of the book came out in 2023. Three years later, I decided that the time had come for an updated edition. I’ve fixed errors, expanded or rewritten some chapters, and added new topics I’d like to cover, such as secure software development (SSD) and AI-generated code.</p><p><strong>How did the book appear?</strong></p><p>Originally, I wanted to write a book about techniques for creating high-quality code. I wanted to cover topics such as code formatting and what to look for during code reviews. I also wished to discuss which new C++ features developers should definitely adopt and which ones might be questionable. However, I got caught up in another topic by accident and decided to go with the flow.</p><p>It all started with a humorous post on Habr, where I came up with a few pieces of bad advice and invited readers to contribute their own. The community liked the idea and shared lots of suggestions for writing truly terrible code. I picked some of those tips, added my own, and posted them in an article. I also explained some tips that beginners might not realise could be harmful. That’s how the idea took shape and evolved into this book.</p><p>I enjoyed writing about what not to do rather than how to do things right. It was a nice change of pace, if nothing else. I’m tired of writing traditional educational materials, particularly those on secure software development. Sometimes, I just want to let loose, add some humour and see where my thoughts take me.</p><p>I’ve spent so many years studying software bugs and writing about how to avoid them. This is closely linked to my work on developing and promoting PVS-Studio, a static code analyzer.</p><p>Static analyzers detect code fragments that are likely to contain bugs or violate a project’s coding standards. PVS-Studio is one such tool. The tool’s mascot, Cody the unicorn, will make a few more appearances throughout the book.</p><p>I use open-source projects to show the types of bugs and potential vulnerabilities this methodology can uncover. Even more importantly, though, I try to explain how to write code that’s free of such errors. In other words, I keep coming back to the same topics: how to properly format code, what makes a good comment, why code reviews matter, and so on.</p><p>So, I decided to expand on the concept of terrible tips and turn it into a book. However, very little remains from the original list of bad advice, aside from the idea itself. I removed or replaced anything that was too obvious or uninteresting, and I added a lot of theory that will benefit both beginners and seasoned developers. The second edition also covers a wider range of topics.</p><p>As you might expect, I learned what good and bad code looks like by developing PVS-Studio and studying the bugs it detected. This is why the analyzer will occasionally appear in various chapters. I’ve spent so much time working with static analysis that I immediately think of real-world bugs when I hear about certain topics. I can’t resist sharing these examples to make my points more convincing. If they help illustrate the point, why not mention them?</p><p>I wrote this book with love and respect for my readers, and I didn’t use AI to generate the text. I hope you enjoy reading it!</p><h3>Terrible tip N1. Only C++</h3><p><strong>Real developers code only in C++!</strong></p><p>There’s nothing wrong with writing code in C++. There are many projects in the world written in C++. Take a look at <a href="https://www.stroustrup.com/applications.html">this list from Bjarne Stroustrup’s homepage</a>, for example.</p><blockquote><em>“Here is a list of systems, applications, and libraries that are completely or mostly written in C++. Naturally, this is not intended to be a complete list. In fact, I couldn’t list a 1000th of all major C++ programs if I tried, and this list holds maybe 1000th of the ones I have heard of. It is a list of systems, applications, and libraries that a reader might have some familiarity with, that might give a novice an idea what is being done with C++, or that I simply thought “cool”.</em></blockquote><blockquote><em>Bjarne Stroustrup</em></blockquote><p>It’s bad when developers use C++ just because it’s “cool” or because it’s the only language the team knows.</p><p>The variety of programming languages reflects the wide range of tasks software developers encounter. Different languages help developers solve different types of problems.</p><p>C++ is a versatile programming language. However, being versatile doesn’t guarantee that particular applications will be implemented quickly and easily. Some languages may be better suited to certain projects than others.</p><p>It’s fine for a team to develop a small utility in C++, even if another language would be more efficient. The time and effort required to learn a new language may be greater than the benefits it provides.</p><p>However, it’s a different story when the team needs to build a large project. At that point, it’s worth taking a step back and asking: Is C++, a language we know well, really the most efficient choice for this project? Would another language be a better fit?</p><p>If using another language would clearly be more efficient, it may make sense for the team to invest time in learning it. In the long run, this could greatly reduce development and maintenance costs. As an alternative, the team could hand the project over to another group that uses a more suitable language for the task.</p><p>So, when should you choose C++? C and C++ have been the most successful languages for system programming tasks, such as operating systems, drivers, compilers, and DBMSs. They’re also well-suited for environments with limited resources, such as memory and CPU time.</p><p>When we talk about resource constraints, microcontrollers are probably the first thing that comes to mind. Yes, C++ is widely used in embedded systems and the IoT (Internet of Things).</p><p>However, resource constraints can also take other forms. C++ is also used to develop applications for high-performance computing clusters with enormous amounts of memory. So, where’s the catch? In this case, the constraints are the cost of the necessary hardware and the electricity required to power the data centers. Even a small improvement in code efficiency — just 5–20% — can result in significant savings.</p><p>Another story that comes to mind is how Disney Studios reduced rendering time by developing its own C++ algorithms that used SIMD and specialized data structures.</p><p>To sum up, C++ is an excellent language for building highly efficient applications. The trade-off is a steep learning curve and the necessity of strict discipline when writing code. Choose C++ when you genuinely need it.</p><p>You might be tempted to argue that developers no longer need an in-depth knowledge of the language because generative AI (GenAI) can now write code for them. So, why not choose C++ and start generating fast code right away? With that, we come to our next terrible tip.</p><h3>Terrible tip N2. Don’t learn C++; command it with vibe coding</h3><p><strong>You no longer need an in-depth knowledge of programming languages. Thanks to GenAI, you can write efficient C++ applications without having to spend time learning the language.</strong></p><p>Writing about GenAI is a thankless task. AI-driven development paradigms are evolving and changing so rapidly that something may already be outdated by the time I finish writing this book. And by the time it’s printed, I’ll probably wish I’d written this chapter differently. Still, I’ll try to express an important idea that will survive the AI gold rush.</p><p>Without understanding how the generated code works, developers risk falling into two traps:</p><ul><li>They won’t be able to tell efficient code from inefficient one.</li><li>They’ll encounter an error they can’t fix. They may try to fix it by repeatedly regenerating the code, but that might not work either.</li></ul><p>While these problems can arise when working with any programming language, they’re particularly pronounced in C++. C++’s high performance comes at the price of its complexity. Writing inefficient C++ code is like using a microscope to hammer a nail.</p><p>In this chapter, we’ll focus on the first point. The next chapter will take a closer look at errors and dangers of placing too much trust in AI-generated code.</p><p>GenAI can create code that looks elegant and optimized. Is it really, though? Only a C++ expert with an in-depth understanding of the language is qualified to verify the soundness of the project’s architecture, as well as the efficiency of its algorithms and code. Without such expertise, the project can quickly go off track.</p><p>For example, let’s look at a small function generated with Claude Opus. The comment above IsSpanBlank promises that the code is structured to make the compiler&#39;s job easier when vectorizing it, for example, by using SSE instructions.</p><p>The following example comes from the article “<a href="https://pvs-studio.com/en/blog/posts/cpp/1366/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1426">Let’s check vibe code that acts like optimized C++ but is actually a mess</a>”. The article also provides other examples that demonstrate why developers need to manually check AI-generated code. Otherwise, code will become complex, bloated, and slow. I recommend taking a few minutes to read through it: it’s entertaining and educational.</p><p>The key to the optimization is that it handles multiple elements at once to see if they have space characters:</p><pre>// Check if a span contains only blank characters<br>// (space, tab, \r, \n)<br>// SIMD-friendly: processes 8 bytes at a time<br>inline bool IsSpanBlank(const char* data, size_t len) {<br>  size_t i = 0;<br>  // Process 8 bytes at a time<br>  for (; i + 8 &lt;= len; i += 8) {<br>    // For each byte, check if it&#39;s NOT a blank character<br>    // Blank chars: space(0x20), tab(0x09), \n(0x0A), \r(0x0D)<br>    bool all_blank = true;<br>    for (size_t j = 0; j &lt; 8; ++j) {<br>      unsigned char c = static_cast&lt;unsigned char&gt;(data[i + j]);<br>      bool is_blank = (c == &#39; &#39; || c == &#39;\t&#39; ||<br>                       c == &#39;\n&#39; || c == &#39;\r&#39;);<br>      all_blank = all_blank &amp;&amp; is_blank;<br>    }<br>    if (!all_blank) return false;<br>  }<br>  // Handle remaining bytes<br>  for (; i &lt; len; ++i) {<br>    unsigned char c = static_cast&lt;unsigned char&gt;(data[i]);<br>    if (c != &#39; &#39; &amp;&amp; c != &#39;\t&#39; &amp;&amp; c != &#39;\n&#39; &amp;&amp; c != &#39;\r&#39;) {<br>      return false;<br>    }<br>  }<br>  return true;<br>}</pre><p>The code makes it seem like the AI knows what it’s doing. The inner loop handles eight characters at a time and can be optimized using SIMD instructions. A separate simple loop handles the remaining characters.</p><p>To me, it looked suspicious that the bytes are still being handled sequentially and that the nested loop doesn’t add anything useful. I think this code is just as good as the simplest way to implement it:</p><pre>bool IsSpanBlank_Simple(const char* data, size_t len) {<br>  for (size_t i = 0; i &lt; len; ++i) {<br>    unsigned char c = static_cast&lt;unsigned char&gt;(data[i]);<br>    if (c != &#39; &#39; &amp;&amp; c != &#39;\t&#39; &amp;&amp; c != &#39;\n&#39; &amp;&amp; c != &#39;\r&#39;) {<br>      return false;<br>    }<br>  }<br>  return true;<br>}</pre><p>I’d write the code this way. Had I known this was a bottleneck and I absolutely had to optimize the code as much as possible, I would’ve looked for an intrinsic-based solution.</p><p>I wouldn’t try any other option, such as one with a nested loop. I know a thing or two about optimization, but not enough to dare write fancy code just to give the compiler a hint.</p><p>One could say, “You’re weak! Claude Opus did it — it came up with the smarter version!”</p><p>Let’s compare performance. My colleague Phillip, who’s an expert on C++, conducted a small experiment with GCC and Clang:</p><ul><li>We create the same dataset for both benchmarks: we generate 1,000,000 strings of random lengths, with arbitrary content, ranging from 1 to 127 characters. The calculations don’t include set generation.</li><li>We feed a dataset to the algorithm and determine whether each string contains spaces. We measure the total time.</li></ul><p>In both cases, the code is compiled with aggressive -O3 optimizations. As you can see, in both compilers, the simple function performs better than the one with the nested loop.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ZaKMErGMCCTESk-F.png" /></figure><p>Shall we specify a magic optimization key to better optimize the vibe code? That key doesn’t exist. And even if it does, the compiler will generate an equally efficient version from simple code with a single loop, even without any hints.</p><p>Along with my colleague, I also experimented with the two previously explored options and three new algorithm variants. I used the MSVC compiler. Let’s take a look at the measurement results.</p><p>First, someone suggested improving the code by replacing the original || with |. In our case, short-circuit evaluation only causes more harm than good. Here&#39;s an improved check:</p><pre>bool is_blank = (c == &#39; &#39; | c == &#39;\t&#39; | c == &#39;\n&#39; | c == &#39;\r&#39;);</pre><p>Secondly, I asked DeepSeek. It suggested an optimization approach based on bitwise operations:</p><pre>bool IsSpanBlank_Deepseek(const char* data, size_t len) noexcept {<br>  // Creating a bitmask for valid characters<br>  // Using a 128-bit mask for ASCII<br>  static constexpr uint64_t blank_mask_low = []() {<br>      uint64_t mask = 0;<br>      mask |= 1ULL &lt;&lt; &#39; &#39;;<br>      mask |= 1ULL &lt;&lt; &#39;\t&#39;;<br>      mask |= 1ULL &lt;&lt; &#39;\n&#39;;<br>      mask |= 1ULL &lt;&lt; &#39;\r&#39;;<br>      return mask;<br>  }();<br>    <br>  for (size_t i = 0; i &lt; len; ++i) {<br>    unsigned char uc = static_cast&lt;unsigned char&gt;(data[i]);<br>    if (uc &gt;= 64) return false;  // All blank characters in ASCII &lt; 64<br>    if (!((blank_mask_low &gt;&gt; uc) &amp; 1)) return false;<br>  }<br>  return true;<br>}</pre><p>Finally, I asked DeepSeek for an optimized function that truly takes advantage of vectorization. Here’s what I got:</p><pre>bool IsSpanBlank_Deepseek_SSE(const char* data, size_t len) noexcept {<br>  // Creating vectors with valid characters<br>  const __m128i space = _mm_set1_epi8(&#39; &#39;);<br>  const __m128i tab = _mm_set1_epi8(&#39;\t&#39;);<br>  const __m128i newline = _mm_set1_epi8(&#39;\n&#39;);<br>  const __m128i carriage = _mm_set1_epi8(&#39;\r&#39;);<br>    <br>  size_t i = 0;<br>    <br>  // Processing 16 bytes at a time<br>  for (; i + 15 &lt; len; i += 16) {<br>    __m128i chunk =<br>      _mm_loadu_si128(reinterpret_cast&lt;const __m128i*&gt;(data + i));<br>       <br>    // Checking that each byte matches one of the valid values<br>    __m128i eq_space = _mm_cmpeq_epi8(chunk, space);<br>    __m128i eq_tab = _mm_cmpeq_epi8(chunk, tab);<br>    __m128i eq_nl = _mm_cmpeq_epi8(chunk, newline);<br>    __m128i eq_cr = _mm_cmpeq_epi8(chunk, carriage);<br>       <br>    __m128i is_blank = _mm_or_si128(_mm_or_si128(eq_space, eq_tab),<br>                                         _mm_or_si128(eq_nl, eq_cr));<br>        <br>    // Obtaining the mask and checking it<br>    int mask = _mm_movemask_epi8(is_blank);<br>    if (mask != 0xFFFF) return false;<br>}<br>    <br>  // Processing the remainder<br>  for (; i &lt; len; ++i) {<br>    char c = data[i];<br>    if (c != &#39; &#39; &amp;&amp; c != &#39;\t&#39; &amp;&amp; c != &#39;\n&#39; &amp;&amp; c != &#39;\r&#39;) return false;<br>  }<br>    <br>  return true;<br>}</pre><p>Now, the results. Although 32-bit builds are no longer relevant, I ran it just for the sake of curiosity. Testing was performed on a 212-megabyte array filled with random space characters. Thread execution is bound to a single preheated core.</p><pre>Win32:<br>The simplest option with a single loop:                          1.39 s.<br>The original &quot;optimized code&quot; with a nested loop:                1.86 s.<br>The original version but the || operators were replaced with | : 0.351 s.<br>Deepseek&#39;s implementation with bitmask optimization:             0.817 s.<br>Deepseek&#39;s implementation with SSE2 intrinsic optimization:      0.0449 s.</pre><pre>Win64:<br>The simplest option with a single loop:                          0.175 s.<br>The original &quot;optimized code&quot; with a nested loop:                0.309 s.<br>The original version but the || operators were replaced with | : 0.253 s.<br>Deepseek&#39;s implementation with bitmask optimization:             0.207 s.<br>Deepseek&#39;s implementation with SSE2 intrinsic optimization:      0.0396 s.</pre><p>Clearly, the 64-bit code is much more efficient, except when SSE2 is involved. Even then, though, everything runs quickly.</p><p><strong>The code Claude Opus generated for the markus project is the worst of all the options.</strong></p><p>Not only is the simplest implementation with a regular loop faster, it’s also shorter. The extra code lines only made things worse.</p><p>Although intrinsic code is in the lead, this comes at the cost of limited portability.</p><p>Some might say I’m worrying for nothing. Yes, Claude Opus chose a poor implementation, but DeepSeek offered a perfectly good, fast, and portable one.</p><p>I agree. However, we see some poorly written code in the project. It looks optimized, but it isn’t. The issue remains unresolved. This case requires an expert who understands it and can come up with a different solution. Knowing C++ well isn’t enough to be one. It’s also necessary to have experience writing and optimizing applications.</p><h3>Terrible tip N3. Reading books is no longer relevant</h3><p><strong>Reading fundamental books on C++ is a waste of time. Educational videos are enough, GenAI will help with everything else.</strong></p><p>This chapter is for those who are just getting started with programming. Experienced developers can skip it. Or they can read it anyway and see how much it resonates with their own thoughts and experiences.</p><p>The point of the previous chapter wasn’t to discourage the use of GenAI. Instead, the focus is on understanding and controlling whether the generated code meets the necessary quality, reliability, and security standards. This brings us to the question of learning programming theory and gaining hands-on experience.</p><p>The question of how to learn programming has always existed. The issue has become even more pressing today because of the illusion that programming technologies are evolving faster than ever before. In their articles, GenAI and vibe-coding enthusiasts openly claim that knowledge acquired just a year ago is obsolete and that developers should use AI agents differently. Maybe the problem is that what they had wasn’t real knowledge to begin with.</p><p>Fundamental knowledge of system design, OOP, programming languages, team management, and so on remains just as relevant as ever.</p><p>However, all the hype surrounding GenAI may cause inexperienced developers to believe that truly investing in self-education is pointless since everything will become obsolete in just a year. Instead, they should focus on quickly learning the basics of whatever is new while GenAI successfully implements everything they could ever hope to.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qJnQKlrq-QzwT3xT.png" /></figure><p>Ensuring that everything works correctly and efficiently requires understanding of the process. Sometimes, it’s optional: for example, when building an MVP or working on a pet project. However, I’m referring to professional software development, where tasks and their complexity extend well beyond the scope of quickly generating some code. GenAI enthusiasts who write articles about it often don’t even understand the problems we’re dealing with. They see the code and think, “There it is. What more could you want?” I like this quote that captures the idea:</p><blockquote><em>“And that, in my opinion, is one of the main problems right now. Those who don’t fully understand programming are now confidently telling everyone else how to do it.”</em></blockquote><blockquote><em>Mikhail Larchenko.</em></blockquote><p>For anyone getting started in programming, my advice is to learn and practice writing code on your own. These skills are necessary regardless of whether the code is AI-generated or written by a human developer. That distinction doesn’t matter. What matters is having experts who can take responsibility for the results!</p><p>What pitfalls await those who want to learn programming today?</p><ul><li>When studying theory: the short-form video format that the social media era has made so prevalent.</li><li>When practicing: the temptation to turn to GenAI right away.</li></ul><h3>Theory</h3><p>I’m all for reading books on programming. However, I can’t downplay the benefits of tutorial videos and other learning methods. I’d enjoy watching a talk from a C++ conference.</p><p>The problem lies in the growing preference for video-based content. I’ve noticed that reading programming books has gone out of fashion.</p><p>Educational videos have two major drawbacks:</p><ul><li>They don’t encourage you to stop and reflect on what you’ve seen and heard. You can pause or rewind, but neither feels as convenient or natural as reading a book. When studying C++ from books, I would often reread paragraphs or go through example code again, taking my time. In other words, I’m talking about attentive and thoughtful reading. You can use videos to achieve the same results, but it doesn’t work quite the same way. You just keep watching, even when you haven’t fully understood something. Sometimes, you even speed up the video.</li><li>Unlike books, educational videos lack a comprehensive approach to information. I can’t imagine how anyone could fit the same amount of knowledge into a video in an accessible way as, for example, Bjarne Stroustrup presents across 1,200 pages in “The C++ Programming Language” (4th edition).</li></ul><p>If approached with the same attention to detail as books, a video would look something like this…</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3a5u9gugpADB2D3A.png" /></figure><p>Videos are a demanding format to learn from. In my opinion, they’re most effective when used to reinforce existing knowledge. They’re useful when they build on a solid foundation acquired through books. Otherwise, they create a fragmented illusion of knowledge. A developer may appear to know their way around programming, but these kinds of knowledge gaps will eventually catch up with them and their employer.</p><p>Even before the GenAI era, I’ve encountered programmers like this. At first glance, they seem incredibly talented. They can quickly build something at a hackathon using trendy libraries or create something stunning with a game engine. It all looks like magic, especially if you’ve never worked with those libraries yourself. However, this illusion quickly fades when they struggle with a real-world task because they’re unaware of basic algorithms or the intricacies of their programming language. Now, in the age of GenAI, things are only going to get worse.</p><p>So, what’s the problem if someone doesn’t know everything, yet still manages to get things done? The problem is inefficiency and limited capabilities. A lack of knowledge can lead people to adopt strange, highly inefficient approaches. They may rely on vibe coding or search Stack Overflow for ready-made solutions. However, sooner or later, they’ll reach a point where they can’t solve the problems they’re facing. The line isn’t visible, but it’s there.</p><p>By the way, I ask candidates during job interviews which C++ programming books they’ve read. Not having read any books doesn’t automatically disqualify someone, but it does raise a red flag.</p><h3>Practice</h3><p>A beginner who uses GenAI to write code from day one skips the gradual learning process from syntax to architecture. They get a working result right away without first understanding why one approach works better than another. They neither learn from their mistakes nor reflect on their actions.</p><p>Getting quick, working results is exciting and motivating. However, not understanding how the system works as a whole is a big issue. It’ll catch up with them later when they finally have to figure out why AI generated something that doesn’t work.</p><p>When you learn programming, with GenAI writing code for you, you don’t have to track down bugs through debugging or understand the intricacies of how an algorithm works. Instead, you’re encouraged to fix issues by simply regenerating the code. In a real-world project, that lack of debugging experience will eventually lead to a dead end.</p><p>It’s up to you to decide what to do and whether to take my advice. And my friendly advice is to spend less time reading about the AI hype. Explore classic programming books, practice writing code on your own, and make it work. We’ll always need experts who truly understand what they’re doing.</p><h3>A look back</h3><p>Sometimes it’s interesting to look at something new through the lens of the past. Here’s a quote from a wonderful book “Code Complete” by Steve McConnell (second edition, ISBN 978–5–7502–0064–1).</p><blockquote><em>“Over the past several decades programmers have seen numerous tools that were supposed to eliminate programming. First it was third generation languages. Then it was fourth generation languages. Then it was automatic programming. Then it was CASE tools. Then it was visual programming. Each of these advances spun off valuable, incremental improvements to computer programming — and collectively they have made programming unrecognizable to anyone who learned programming before these advances. But none of these innovations succeeded in eliminating programming.”</em></blockquote><p>…</p><blockquote><em>“We will always need people who can bridge the gap between the real world problem to be solved and the computer that is supposed to be solving the problem. These people will be called programmers regardless of whether we’re manipulating machine registers in assembler or dialog boxes in Visual Basic. As long as we have computers, we’ll need people who tell the computers what to do, and that activity will be called programming. When you hear a tool vendor claim, “This new tool will eliminate computer programming” — run! Or at least smile to yourself at the vendor’s naive optimism.”</em></blockquote><p>As they say, Simpsons already did it. The book was published in 2004.</p><h3>Terrible tip N4. Tab characters in string literals</h3><p><strong>If you need a tab character in a string literal, just press the Tab key. Leave \t to the nerds. Don’t sweat it.</strong></p><p>I’m talking about string literals where tabs separate words:</p><pre>const char str[] = &quot;AAA\tBBB\tCCC&quot;;</pre><p>It’d seem there’s no other way to do it. Yet, sometimes, developers just carelessly press the Tab key instead of using \t. This thing happens even in real projects.</p><p>Such code compiles and even works. However, explicitly using the tab character isn’t recommended for the following reasons:</p><ul><li>Literals may have spaces instead of tabs, which depends on the editor settings. Although, the code looks like there’s a tab character.</li><li>The developer who maintains the code won’t immediately understand whether tabs or spaces are used as delimiters.</li><li>During refactoring (or using code autoformatting utilities), tabs may turn into spaces, which affects the program’s behavior.</li></ul><p>Moreover, once I saw the code similar to this one in a real application:</p><pre>const char table[] = &quot;\<br>bla-bla-bla bla-bla-bla bla-bla-bla-bla-bla \n\<br>      bla-bla-bla       bla-bla-bla\n\<br>          %s                %d\n\<br>          %s                %d\n\<br>          %s                %d\n\<br>&quot;;<br>  printf(table, &quot;X&quot;, 1, &quot;Foo&quot;, 2, &quot;Time&quot;, 3);</pre><p>The ‘\’ character splits the string into parts. Explicit tab characters are mixed with space characters. Unfortunately, I don’t know how to show it here, but trust me, the code looked bizarre. On top of that, the alignment started at the beginning of the screen. Bingo! I’ve seen lots of strange things while developing a code analyzer!</p><p>Explicit tabs added using the Tab key are very easy to &quot;lose&quot;. If they&#39;re replaced with spaces during refactoring or auto-formatting, values will no longer print in a column:</p><pre>bla-bla-bla bla-bla-bla bla-bla-bla-bla-bla<br>     bla-bla-bla       bla-bla-bla<br>         X                  1<br>         Foo                  2<br>         Time                  3</pre><p>The code should’ve been formatted as follows:</p><pre>const char table[] =<br>  &quot;bla-bla-bla bla-bla-bla bla-bla-bla-bla-bla\n&quot;<br>  &quot;     bla-bla-bla       bla-bla-bla\n&quot;<br>  &quot;         %s\t            %d\n&quot;<br>  &quot;         %s\t            %d\n&quot;<br>  &quot;         %s\t            %d\n&quot;;</pre><pre>printf(table, &quot;X&quot;, 1, &quot;Foo&quot;, 2, &quot;Time&quot;, 3);</pre><p>Tab characters ensure that the second column is aligned, regardless of the name length in the first column (X, Foo, Time). These names are expected to be short—fewer than eight characters long. The fixed code will print:</p><pre>bla-bla-bla bla-bla-bla bla-bla-bla-bla-bla<br>     bla-bla-bla       bla-bla-bla<br>         X                 1<br>         Foo               2<br>         Time              3</pre><p>Some people might wonder why I’m talking about such simple and obvious things. Moreover, even the fixed code is a bit off and doesn’t look like a good example for the book. However, since I could clearly see tab characters in the lines, I think it’s worth mentioning. This fix is better than having someone repeatedly pressing the Tab key without understanding what&#39;s wrong with it.</p><p><strong>Author: Andrey Karpov</strong></p><p>Andrey has over 19 years of experience with static code analysis and software quality. The author of numerous articles on writing high-quality code in C++. Andrey Karpov has been honored with the Microsoft MVP award in the Developer Technologies category from 2011 to 2021. Andrey is a co-founder of the PVS-Studio project. He has long been the company’s CTO and was involved in the development of the C++ analyzer core. Andrey is currently the Chief Business Development Officer (CBDO), responsible for educational marketing.</p><h3>To be continued</h3><p>In upcoming posts, you’ll find more terrible tips for C++ programmers. Make sure you <a href="https://pvs-studio.com/en/subscribe/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1426"><strong>subscribe to our blog newsletter</strong></a> to stay up to date with all our latest content!</p><p><strong>Follow Andrey Karpov on </strong><a href="https://uz.linkedin.com/in/andrey-karpov-pvs-studio"><strong>LinkedIn</strong></a><strong> and </strong><a href="https://x.com/Code_Analysis"><strong>X</strong></a><strong>.</strong></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7be5db120bd5" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/terrible-tips-for-a-c-developer-2026-edition-part-1-7be5db120bd5">Terrible tips for a C++ developer: 2026 edition, Part 1</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Unity3D for .NET Developers: How to get started]]></title>
            <link>https://medium.com/pvs-studio/unity3d-for-net-developers-how-to-get-started-71dacd8fc061?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/71dacd8fc061</guid>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[game-development]]></category>
            <category><![CDATA[unity3d]]></category>
            <category><![CDATA[csharp]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Tue, 03 Nov 2026 12:48:04 GMT</pubDate>
            <atom:updated>2026-11-03T12:48:04.687Z</atom:updated>
            <content:encoded><![CDATA[<p>As a C#/.NET developer, have you ever thought about getting into game development with Unity3D? After all, it uses the same language. Is it really the same, though? Can you simply copy code written for .NET into a Unity3D application and expect it to work without any issues? Let’s take a closer look to find out what extra knowledge C#/.NET developers need to comfortably and effectively develop games.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DJNutS-UNpbrtyWx.png" /></figure><blockquote><em>We published and translated this article with the copyright holder’s permission. The author is </em><a href="https://github.com/kekchpek/"><em>Kekchpek</em></a><em>.</em></blockquote><h3>Compilation and execution</h3><p>First, we’ll compare how our code is compiled and executed in .NET and Unity3D. In .NET, the process is fairly straightforward and transparent. The C# code is first compiled into IL, then packaged into DLL or EXE files, and finally executed by a virtual machine. However, this approach has one drawback that has long posed a major obstacle to cross-platform development for C# programmers: dependence on a virtual machine. Today’s .NET is nothing like its predecessor, .NET Framework, which was exclusively Windows-based. Before .NET emerged, the most popular solution for cross-platform development was the Mono virtual machine, which could also execute IL code and served as an alternative to .NET Framework.</p><p>Unity3D originally ran on the Mono virtual machine. Today, Mono remains one of the options for running C# code in Unity3D. The second option is IL2CPP, a newer technology developed by Unity Technologies, which is gradually replacing Mono. Code compiled with IL2CPP runs faster and doesn’t require a pre-installed or bundled virtual machine. As for how IL2CPP works, its name speaks for itself. Roslyn first compiles C# into IL, which is then translated into C++. Finally, the C++ compiler converts the code into machine code. The C++ compiler varies depending on the target platform. In terms of C#, however, the code behaves much like it does in .NET. Unity3D uses a very similar garbage collector, standard library, and so on.</p><p>So, although the compilation process differs, the average developer won’t notice much difference. One drawback is that it’s nearly impossible to predict how the code will compile on different platforms. IL2CPP bugs are certainly a possibility, but encountering them is as unlikely as running into bugs in .NET.</p><p>Any C# DLL libraries that contains IL can be used in Unity3D. During compilation, Unity runs the code through IL2CPP and recompiles it into C++.</p><p>However, things aren’t that simple when it comes to C# versions. Unity uses C# 9, which is rather outdated, and even that version lacks support for certain features. You can read more about it <a href="https://docs.unity3d.com/2022.3/Documentation/Manual/CSharpCompiler.html">here</a>.</p><h3>Libraries and compatibility</h3><p>As I mentioned earlier, libraries written in C# can be compiled for different platforms using IL2CPP, but there are some limitations. Unity supports compatibility with .NET Framework and .NET Standard libraries. However, that doesn’t mean libraries compiled for .NET won’t work. They might work, but not all of them will. It depends on whether their IL code contains anything that Unity3D can’t translate into C++.</p><p>The standard library works in Unity3D, but with a few caveats. In other words, we have access to all the features of standard C#, including collections, concurrency, Tasks, LINQ, networking, and more. However, Thread and Task (when used to run separate threads), for example, won&#39;t work in WebGL builds (games that run in browsers). This is one of the most obvious limitations, but one may occasionally encounter others related to the specifics of a particular platform.</p><p>From a coding perspective, Unity3D acts as an extensive library within the UnityEngine namespace. The UnityEditor namespace also exists that contains the editor API, which enables developers customize windows, builds, plugins, and more.</p><h3>How to organize code into modules</h3><p>In .NET, we’re used to organizing code into separate modules via .sln and .csproj files. Unity3D has them too, but we can&#39;t work with them directly. Unity3D creates .sln and .csproj files, purely for the developers&#39; convenience. This allows popular IDEs to provide proper syntax highlighting, error detection, and other useful features. Unity itself doesn&#39;t rely on the contents of .sln or .csproj files at any stage of the project build process. As a result, one common mistake beginners make is adding a library via their IDE. This only modifies the .csproj file, but Unity remains unaware of the change. So, when a dev tries to compile the project, Unity starts throwing errors even though the IDE shows no errors at all.</p><p>To control how Unity3D splits code into modules and what properties those modules have, we need to use Unity’s built-in tools. The main feature we’ll work with here is Assembly Definition, Unity’s equivalent of a .csproj file in .NET. To use Assembly Definition, we need to create an .asmdef file in one of the project folders. We can do this via the Unity graphical interface. Once we create the file, Unity treats all .cs files in that folder and its subfolders as a separate module. It generates a dedicated .csproj file for this code and enables us to specify dependencies required by that particular module. We can also use the module as a dependency. We configure everything using Unity3D&#39;s graphical interface, and Unity reflects those settings in the generated .csproj files.</p><p>By default, we don’t create any .asmdef files, Unity automatically divides code into four modules based on the names of the folders containing the .cs files:</p><ul><li>Assembly-CSharp-Editor-firstpass: the “*/Standard Assets/**/Editor”, “*/Pro Standard Assets/**/Editor” and “*/Plugins/**/Editor” folders</li><li>Assembly-CSharp-firstpass: the “*/Standard Assets”, “*/Pro Standard Assets” and “*/Plugins” folders</li><li>Assembly-CSharp-Editor: the “*/Editor” folders</li><li>Assembly-CSharp: any other folder</li></ul><p>Using Assembly Definitions is generally considered a good practice, though not every project has them. The key point to keep in mind is the difference between the Editor and Runtime modules. Editor contains code that runs only in the Unity editor and doesn’t make it into the game build. Developers usually use this code to create additional tools for game designers, artists, and programmers. We can specify in any Assembly Definition that it’s intended only for the editor.</p><h3>How to integrate third-party libraries</h3><p>The easiest way to add a third-party library is to simply copy its DLL into the project folder. Unity automatically scans the project folder for DLLs and adds them as dependencies to the generated .csproj files. In the Assembly Definition settings, we can explicitly specify which DLLs to include as dependencies and which to exclude.</p><p>We can also directly add source code instead of using DLLs. This might seem a little strange to a .NET developer, but it’s actually a fairly common way to distribute plugins and SDKs for Unity3D. Unity even has a dedicated packaging mechanism called .unitypackage. These files simplify the distribution of assets that need to be imported into specific parts of a project. Usually, when we want to download some open-source SDK, we get a .unitypackage file from GitHub and open it in the Unity editor. The editor adds the necessary source files to the project or updates existing ones, first enabling a developer to review the package contents and choose which files to import. Some SDKs even offer to remove outdated files from previous versions after importing the .unitypackage. However, I&#39;m not sure whether this feature is built into the Unity editor or if SDK developers implemented it themselves.</p><p>A more convenient way to manage dependencies is the Unity Package Manager. It’s a package manager similar to npm, with a graphical interface built into the Unity Editor. As with npm, each package used in the project has a package.json file that describes the package and its dependencies. Every package stores this file in its root directory.</p><p>The project itself, however, uses manifest.json instead of package.json, unlike npm. Despite the different name, manifest.json serves a similar purpose: it lists the project&#39;s dependencies and the package registries from which Unity can retrieve them. Instead of specifying a version, we can provide a link to a GitHub repository or a specific directory within that repository from which Unity should download the dependency. We can also use links to private repositories after configuring <a href="https://docs.unity3d.com/Manual/upm-config-https-git.html">Git Credential Manager</a>. Unfortunately, we can specify GitHub links only for dependencies listed in manifest.json.</p><p>In other words, we can’t use GitHub links for transitive dependencies (dependencies of other dependencies). This limitation makes managing dependencies less convenient when building infrastructure for large-scale projects. One solution is to create our own package registries and host them in the cloud. These registries can be either private or public.</p><p><a href="https://openupm.com/">OpenUPM (Open Unity Package Manager)</a> deserves a special mention. It’s an open package registry for the standard Unity Package Manager where anyone can publish their packages. Most packages distributed as .unitypackage files are also available on OpenUPM. There are also many NuGet packages available on OpenUPM. However, not every NuGet package developer—or developer of any other package—takes the time to publish their packages on OpenUPM. As a result, some packages listed on OpenUPM are just forks of the original projects, tweaked to work with OpenUPM. I&#39;d still advise against dismissing these packages outright.</p><p>If you’re a company that takes its code supply chain seriously and carefully controls what goes into its projects, the best approach is to set up your own package registry. You can then validate incoming packages and download everything from that registry. But if you’re developing a small game, a prototype, or if you simply lack the resources to maintain a robust infrastructure for your project, OpenUPM is an excellent solution.</p><p>And since this article is aimed at .NET developers, I’d like to mention <a href="https://github.com/GlitchEnzo/NuGetForUnity">NuGet for Unity</a>. It’s a plugin that adds a user-friendly interface for managing NuGet packages in Unity. I wouldn’t recommend it, despite having used it a lot myself. The main disadvantage of using NuGet is that it brings multiple sources and dependency managers into a project. This can quickly turn into a major headache in large projects with lots of dependencies. Managing package versions and resolving conflicts between transitive dependencies can be serious challenges.</p><p>In short, I recommend using the Unity Package Manager alongside OpenUPM and other package registries wherever possible. Resort to other ways of adding dependencies only when the Unity Package Manager isn’t an option.</p><h3>Unity3D architecture</h3><p>Like many game engines, Unity3D uses a tree-based structure to organize objects. An application contains multiple scenes (UnityEngine.Scene), and each of them contains numerous game objects (UnityEngine.GameObject) that act as containers for components (UnityEngine.Component). A component is a basic logical unit that contains executable code. Game objects (GameObject) can also be arranged into hierarchical structures.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/621/0*t_biYzozORBJlGA3.png" /></figure><p>Every GameObject has one mandatory component that can&#39;t be removed: UnityEngine.Transform. It controls the object&#39;s position in the scene and its place in the hierarchy compared to other GameObjects. We can add or remove other components either while working with the scene in the editor or at runtime through code.</p><h3>Entry point</h3><p>I believe we have enough context now to move on to the more hands-on topics. The application entry point is the best place to start. In Unity3D, we don’t have access to the Main function. Instead, we have to work within Unity&#39;s architecture, which loads the first scene in the scene list when the application starts. The only option available to us is to create a custom component and then link it to an object in the starting scene. Once the scene has loaded, the code in that component will start executing.</p><p>To create a component, we’ll create a class that inherits from UnityEngine.MonoBehaviour. Here&#39;s a simple example of a custom component:</p><pre>using UnityEngine;</pre><pre>public class MyComponent : MonoBehaviour {</pre><pre>  private void Start() {<br>    // do something on component start working<br>  }</pre><pre>  private void Update() {<br>    // do something every frame<br>  }</pre><pre>}</pre><p>Now, we can use the editor to create as many instances of this class as needed and attach them to any GameObject in the scene. Each component&#39;s Start method will be called at the beginning of its lifecycle, while Update will run every frame. Unity orders all these method calls according to its <a href="https://docs.unity3d.com/2022.3/Documentation/Manual/ExecutionOrder.html">Script Lifecycle</a>, which is a key architectural element in Unity3D. When designing how code interacts with the engine&#39;s API, developers need to understand exactly where each code fragment should run in the cycle.</p><p>This is where Unity3D starts getting quirky. Notice that Start and Update don&#39;t override any base methods; they aren&#39;t part of an interface, and aren&#39;t even public. Yet third-party code still calls them. We rarely see this approach in .NET, where such things usually rely on reflection. Unity, however, can afford to bend the language rules. Acting as a compiler, it integrates code into its event system without requiring the explicit language constructs normally used for this purpose. In other words, Unity binds methods by name, almost as if we were writing JavaScript. The methods can be private or public—it makes no difference. We can also use inheritance with virtual methods. For example, we can create components that inherit from MyComponent and override the necessary virtual methods.</p><p>Unity3D’s event system includes many other methods that Unity calls at various stages of the Script Lifecycle. We could probably call this reflection in terms of the concept, but the way it works is fundamentally different from using System.Reflection or similar runtime tools. All the magic happens at compile time, so it doesn&#39;t affect the performance of such calls.</p><p>Unity provides a class called UnityEngine.UIBehaviour, which can also be inherited from to create components. It&#39;s a MonoBehaviour subclass whose main purpose is to provide a set of empty virtual methods related to Unity UI events. However, nothing stops us from inheriting directly from MonoBehaviour and defining all the necessary methods without overriding any virtual ones. This feels a little odd, as if even the Unity developers haven&#39;t quite settled on a consistent way to best implement Script Lifecycle functions.</p><h3>How to create components</h3><p>We’ve already encountered one unusual aspect of C# code in Unity, and there’s more to come. As I mentioned earlier, we can add components either using the scene editor or at runtime via code. Let’s start with adding them in the editor. This part is straightforward: we select an object in the scene, click the <strong>Add Component</strong> button in the <strong>Inspector</strong> window, choose MyComponent, and Unity adds it to the object. To understand how this works, it&#39;s worth taking a look under the hood.</p><p>Each scene exists as a separate file that contains everything in that scene, including the entire object hierarchy and all its components. We create an instance of our class when we add a component. When we save the scene, Unity serializes that instance into the scene file. Once we build and run the game, Unity deserializes all our components as it loads the scene, creating new instances of their classes. If we apply the UnityEngine.SerializeField attribute to any of our fields, we can save a component with a specific value for that field. The editor even displays a UI control that enables us to edit the field on the component we&#39;ve added. By default, Unity serializes public fields.</p><p>Adding components at runtime is also quite straightforward. We use the GameObject.AddComponent&lt;T&gt;() method for this. It&#39;s an instance method, so we first need to obtain a GameObject instance. This isn&#39;t a problem, as the Unity API provides plenty of ways to retrieve a particular GameObject. The most common approach is to use the MonoBehaviour.gameObject property.</p><p>By the way, the Unity Engine’s API naming conventions may look unusual to .NET developers: properties and fields start with a lowercase letter. Still, in almost every project I’ve seen, developers stick to the naming conventions commonly used in .NET. Here’s an example of a component that adds a copy of itself to the same GameObject three seconds after it starts executing:</p><pre>using UnityEngine;</pre><pre>public class CopySelfComponent : MonoBehaviour {</pre><pre>  private float _passedTime;</pre><pre>  private void Update() {<br>    if (_passedTime &gt; 3f) <br>    {<br>      // gameObject is a public property of MonoBehaviour<br>      gameObject.AddComponent&lt;CopySelfComponent&gt;();<br>    }<br>    else <br>    {<br>      // Time.deltaTime is a static property that stores the delta between<br>      // the start of the previous and current Update phases<br>      // in the Script lifecycle<br>      _passedTime += Time.deltaTime;<br>    }<br>  }</pre><pre>}</pre><p>At this point, an experienced developer might ask, “What about object dependencies?” It’s no secret that OOP revolves around creating various classes and establishing clear, precise, and elegant relationships between them. In most programming languages, including C#, we use constructors to define a class’s dependencies. I’ve already described two ways to create a component, but neither involves a constructor. The reason might blow your mind: constructors for MonoBehaviour subclasses are UB in C#. For those unfamiliar with C++ and the concept of undefined behavior, here&#39;s how I&#39;d describe it: you can use it, but it won&#39;t end well. Whatever you&#39;re trying to achieve, it&#39;ll probably fail. In other words, we can&#39;t use constructors for components.</p><p>There’s a formal alternative: the Awake() method. It&#39;s the first method Unity calls when creating a component, with a few caveats we won&#39;t get into here. However, this method doesn&#39;t take any arguments either. A common workaround for dependency injection is to define a separate method, usually called Construct(...) or Initialize(...), and pass all the required dependencies to it. Many developers will immediately recognize the downsides of this approach: we end up with an object in an inconsistent state immediately after its creation. That&#39;s something we have to live with. The usual solution is to encapsulate object creation in factories or use dedicated DI frameworks with their own APIs for creating objects and components. Here&#39;s what a typical component might look like when written using one of the most popular DI frameworks, Zenject:</p><pre>using UnityEngine;<br>using Zenject;</pre><pre>public class MyComponent : MonoBehaviour {</pre><pre>  // Here, we can&#39;t even use `readonly` for dependencies, <br>  since they change after the object is created.<br>  private IService1 _service1;<br>  private IService1 _service2;<br>  private IService1 _service3;</pre><pre>  // An attribute that tells the DI framework that this is a method <br>  for dependency injection<br>  [Inject]<br>  public void Construct(<br>    IService1 service1,<br>    IService2 service2,<br>    IService3 service3) {<br>    _service1 = service1;<br>    _service2 = service2;<br>    _service3 = service3;<br>  }<br>  ...</pre><pre>}</pre><p>Things aren’t so bad when it comes to classes that don’t inherit from MonoBehaviour. We can use them just as we would in .NET. In large projects, developers typically implement most of the application&#39;s business logic in regular classes and use components as an integration layer with the engine.</p><h3>The intricacies of writing game code</h3><p>Overall, I’ve covered the main differences in the language, but Unity3D development also has a few specific aspects. Remember that games are resource-intensive applications, mainly because of their graphics. However, we also need to handle code more carefully to avoid putting unnecessary strain on a device that’s already working hard. So, here are a few things to keep in mind:</p><h3>Minimize calls to “expensive” APIs</h3><p>The Unity library offers plenty of ways to tank an application’s performance. Almost all API calls that create or access entities are rather slow. Of course, “slow” in this context doesn’t refer to the pace perceived by a human observer; rather, it’s a measure of the speed in terms of CPU standards. Here’s an example of code that gradually changes a UI image’s color from one to another and back again:</p><pre>using UnityEngine;<br>using UnityEngine.UI;</pre><pre>namespace Components<br>{<br>    public class MyBehaviour : MonoBehaviour<br>    {<br>        <br>        // When the scene loads, Unity prioritizes <br>        the serialized field value. <br>        // If the component is being created for the first time, <br>        Unity uses the initialization value instead.<br>        [SerializeField]<br>        private Color _color1 = Color.red;<br>        <br>        [SerializeField]<br>        private Color _color2 = Color.blue;<br>        <br>        private void Update()<br>        {<br>            var img = this.GetComponent&lt;Image&gt;(); // THIS CODE WILL RUN SLOWLY<br>            <br>            // Some simple math is involved here. I&#39;ll spare you the details.<br>            // You can look into it on your own if you&#39;re interested.<br>            img.color = Color.Lerp(_color1, _color2,<br>            Mathf.PingPong(Time.time, 1f));<br>        }<br>    }<br>}</pre><p>If the code accumulates enough of such calls, they’ll significantly impact performance. A better approach is to cache Image in Awake() or declare it as a serialized field so we can assign its value in the editor. We can drag and drop many types derived from UnityEngine.Object into UI fields in the editor to assign references to specific instances like this:</p><pre>using UnityEngine;<br>using UnityEngine.UI;</pre><pre>public class MyBehaviour : MonoBehaviour<br>{<br>    [SerializeField]<br>    private Color _color1 = Color.red;<br>    [SerializeField]<br>    private Color _color2 = Color.blue;<br>  <br>    private Image _image;</pre><pre>    private void  Awake()<br>    {<br>        _image = GetComponent&lt;Image&gt;();<br>    }<br>        <br>    private void Update()<br>    {<br>        _image.color = Color.Lerp(_color1, _color2, <br>        Mathf.PingPong(Time.time, 1f));<br>    }<br>}</pre><p>Or like this:</p><pre>using UnityEngine;<br>using UnityEngine.UI;</pre><pre>public class MyComponent : MonoBehaviour<br>{<br>  <br>  [SerializeField]<br>  private Color _color1 = Color.red;<br>  [SerializeField]<br>  private Color _color2 = Color.blue;</pre><pre>  [SerializeField]<br>  private Image _image;<br>        <br>  private void Update()<br>  {<br>    _image.color = Color.Lerp(_color1, _color2, <br>    Mathf.PingPong(Time.time, 1f));<br>  }<br>}</pre><p>Here are some other commonly used — though not the only — methods that can cause performance issues:</p><ul><li>GetComponent&lt;T&gt;()</li><li>GetComponents&lt;T&gt;()</li><li>GetComponentInChildren&lt;T&gt;()</li><li>GetComponentInParent&lt;T&gt;()</li><li>FindObjectOfType&lt;T&gt;()</li><li>FindObjectsOfType&lt;T&gt;()</li><li>Find(string)</li><li>FindWithTag(string)</li><li>FindGameObjectsWithTag(string)</li></ul><h3>How to create objects</h3><p>Object creation is another aspect of Unity development that deserves careful attention. I’m referring to both Unity3D objects (GameObject and MonoBehaviour) and regular class instances. In short, the best approach for performance is to avoid creating objects altogether. This is why Unity3D developers extensively use object pooling. It&#39;s hardly a groundbreaking idea, and Unity3D already provides a built-in class for this purpose: UnityEngine.Pool.ObjectPool&lt;T&gt;. Why does this matter in the game development industry? There are different reasons for Unity3D objects and regular class instances.</p><p>For regular class instances, the issue is straightforward: memory allocation. One of the biggest threats to application performance is the garbage collector. Just as in .NET, the garbage collector pauses code execution to manage memory. Creating large numbers of short-lived objects can trigger frequent garbage collection, causing FPS drops during those pauses. Developers may encounter this issue when their code creates various collections to process and transfer data, DTOs, and similar objects.</p><p>Although they usually have a longer lifespan, memory allocation for GameObject instances and components is another concern. The issue once again lies with the slow API responsible for creating them. Sometimes, we need to transition from one game state to another while displaying large numbers of objects along the way. For example, we might load an environment, spawn a large number of units, or create bullets when firing a weapon, which is a now-classic use case. Our goal in these situations is to create game objects as quickly as possible to prevent FPS drops and loading screens that frustrate players. That&#39;s where object pooling comes in, ideally with the warm pool. However, we shouldn&#39;t forget about the balance between memory usage and performance. Pooling too many objects will cause the application to use too much memory, creating its own set of problems. So, we should make smart use of the device&#39;s available resources.</p><h3>Structs</h3><p>For the same performance reasons, it’s a useful skill for any developer to know when to use a struct instead of a class, as this helps reduce the load on the garbage collector. I won’t go into much detail here, since the rules are roughly the same as in .NET. It’s also important to remember about boxing and avoid unnecessarily allocating structs on the heap.</p><h3>How to destroy objects</h3><p>When it comes to destroying objects, Unity has a small peculiarity. Regular classes depend on the garbage collector. However, things become more complicated when it comes to objects that are part of the engine. Let’s take GameObject, for example. When we write new GameObject(), we create a new instance of the GameObject class. At the same time, Unity adds the object to the hierarchy of the currently open scene. It may seem obvious that if the code no longer contains any references to this instance, it will still be part of the scene and will not be removed by the garbage collector.</p><p>To explicitly remove the object from the scene, we need to call the UnityEngine.Object.Destroy(UnityEngine.Object) static method and pass the instance as an argument. This may seem obvious for GameObject and MonoBehaviour subclasses. We know these objects are part of the scene, so it makes sense that we need to take explicit action to remove them. However, there are less obvious cases, too.</p><p>For example, the Texture2D class is an abstraction of a texture, which is an image. We can create a Texture2D instance and assume that we&#39;re the only ones holding a reference to it, so once we lose all references, the garbage collector will remove it. Unfortunately, that&#39;s not how it works. The rule is simple: we should call UnityEngine.Object.Destroy(UnityEngine.Object) for all explicitly created subclasses of UnityEngine.Object. The reason is that C# objects in the Unity API are simply wrappers for native objects belonging to the engine&#39;s C++ code. If we lose all references to our object, only the C# wrapper is removed. However, the C++ object under the hood still occupies memory. On the other hand, calling UnityEngine.Object.Destroy(UnityEngine.Object) destroys only the C++ object. So, after calling UnityEngine.Object.Destroy(UnityEngine.Object), we still have the same C# object, but all of its methods will throw errors because the object that handles the logic has been destroyed.</p><p>We need to be careful when comparing UnityEngine.Object subclasses to null. Unity overrides the equality operator for UnityEngine.Object. So, in addition to checking whether both operands are null, Unity also checks whether the C++ object has been destroyed. If it has, comparing the object to null will return true. This behavior can be confusing and makes comparisons to null relatively expensive. So, it&#39;s better to avoid them in frequently executed code as well.</p><h3>Linq</h3><p>LINQ extension methods are incredibly convenient. They’re also highly effective in middleware-based architectures, where different layers of an application can process the same enumerable object in sequence without repeatedly iterating over its elements. However, there’s one major drawback to using LINQ with Unity3D: its extension methods return objects that require heap allocations.</p><p>This brings us back to the issue of unnecessary object creation we discussed earlier. The overhead is negligible on its own, but constantly using it in frequently executed code can create a problem. I recommend keep using LINQ if you follow the middleware-based approach. However, try to replace LINQ methods with regular for and foreach if you&#39;re using them within a single method.</p><p>Also, be careful with methods that execute LINQ queries, such as ToList(), ToArray(), and so on, as they can significantly increase memory usage. Call them only when you actually need the query result as a concrete collection, or avoid them altogether. That said, I don&#39;t think things are any different here compared to .NET.</p><h3>Concurrency and asynchronous programming</h3><p>Concurrency in Unity3D can be tricky. As I mentioned earlier, code dispatched to another thread simply won’t run at all on WebGL. I haven’t encountered any concurrency issues with how threads behave at the OS level on other platforms. However, almost every Unity API call will throw errors if we try to make it from a thread other than the main one. Experienced Unity developers might point to the Job System, but as far as I know, it only provides limited access to Transform.</p><p>On the one hand, this is nice. We don’t have to struggle too much with synchronization, and neither do Unity3D developers. Many Unity developers don’t even know about the lock keyword, which is considered one of the cornerstones of concurrency in C#. On the other hand, we may sometimes want to use multiple CPU cores to modify the properties of a large number of Unity objects.</p><p>Still, Unity makes extensive use of concurrency under the hood, and its API provides asynchronous operations for loading data from disk, handling network communication, and performing some other resource-intensive tasks. That said, developers at Unity Technologies aren’t particularly fond of the async/await syntax, so we often have to make do with callbacks and events. Of course, nothing stops us from wrapping these in a proper async API using TaskCompletionSource and a few other methods.</p><p>Unity also offers a rather unconventional approach to asynchronous programming called coroutines. Basically, coroutines enable developers to pass an enumeration to the Unity3D engine via its API. Each element tells the engine when to retrieve the next one. When combined with the yield operator, this works as a makeshift version of async/await. It looks something like this:</p><pre>public IEnumerator Coroutine() {<br>  Debug.Log(&quot;Do something&quot;);<br>  <br>  yield return new WaitForNextFrame();<br>  <br>  Debug.Log(&quot;Do something in the next frame&quot;)<br>    <br>  yield return new WaitForSeconds(2f);<br>  <br>  Debug.Log(&quot;Do something after two seconds&quot;);<br>  var t = Task.Delay(4000);<br>  <br>  yield return new WaitUntil(() =&gt; t.Completed);<br>  <br>  Debug.Log(&quot;Do something after 4 more seconds.&quot;);<br>}</pre><pre>...</pre><pre>someMonoBehaviourInstance.StartCoroutine(Coroutine());</pre><pre>...</pre><p>You may have noticed that starting a coroutine requires calling a method on a specific object instance. This tight coupling to an object is one of the advantages of coroutines: Unity cancels a coroutine when we destroy its associated object using UnityEngine.Object.Destroy(obj). In every other respect, though, coroutines are simply a less convenient alternative to async/await.</p><p>Unity3D also has its share of issues with Task. Firstly, it runs on the thread pool, so we run into the same problem: we can&#39;t use the Unity API from those threads. Secondly, Unity3D developers try to avoid allocating memory on the heap, and Task is a class that allocates memory on the heap. ValueTask helps reduce this overhead, as does <a href="https://github.com/Cysharp/UniTask">UniTask</a>, its Unity-specific counterpart. UniTask is an open-source plugin that offers an alternative to ValueTask. It provides additional optimizations and enhances the user experience when working with the Unity API. The library also includes various extension methods for Unity classes, providing convenient ways to use them with await.</p><p>Unity3D also has its own implementation of SynchronizationContext. It slightly streamlines the use of Task and ensures that the code after await won&#39;t run on a different thread. If the object we&#39;re await-ing executes on Unity&#39;s main thread, the code after await will run as soon as the execution finishes. If we use concurrency instead, the code after await will run during an early stage in the Script Lifecycle. WPF uses a similar synchronization context.</p><h3>Covering code with automated tests</h3><p>Unity includes a built-in editor window for running and viewing tests. The test framework is based on NUnit, but there are a few differences. First, Unity supports two types of automated tests: PlayMode and EditMode, which are just regular NUnit tests. We can create usual C# objects, mock dependencies using a mocking library such as NSubstitute (also available through OpenUPM), perform various operations, and check the results. PlayMode tests, on the other hand, run the Script Lifecycle. They enable us to check how the MonoBehaviour subclasses behave over time, how they respond to various physics triggers in the Script Lifecycle, and so on.</p><p>In my experience, relatively few projects have made extensive use of PlayMode tests. Most teams move as much business logic as possible into standard C# classes. This leaves the MonoBehaviour subclasses as a thin layer between the C# architecture and the Unity API. I think the main reason is that testing anything related to the Script Lifecycle can be challenging. The engine&#39;s physics and execution systems don&#39;t provide APIs that offer the level of fine-tuning often required when writing automated tests.</p><p>One disadvantage of testing in Unity is the lack of support for asynchronous tests. This is where the coroutines mentioned above come in, replacing asynchronous operations. It looks something like this:</p><pre>[UnityTest] // This is a special attribute for such tests<br>public IEnumerator AsyncTest() {<br>  // Arrange<br>  SomeService someSerivce = // create some service with mocked dependencies</pre><pre>  // Act<br>  Task&lt;bool&gt; asyncHandle = someService.SomeAsyncMethod();<br>  yield return new WaitUntil(() =&gt; asyncHandle.IsCompleted);</pre><pre>  // Arrange<br>  Assert(true, asyncHandle.Result);<br>}</pre><h3>Conclusion</h3><p>Those were the main points to keep in mind when starting to write C# code in Unity3D. If you have any comments or notice any errors, please leave a comment on the article or open an <a href="https://github.com/kekchpek/articles/tree/master/DotNetToUnity">Issue on GitHub</a>.</p><h3>Want to learn more?</h3><p>The PVS-Studio team loves to publish valuable content for C# developers and folks working in game development. <a href="https://pvs-studio.com/en/subscribe/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1424">Subscribe to our newsletter</a> to follow the articles from the PVS-Studio website.</p><p>Here are some articles:</p><ul><li><a href="https://pvs-studio.com/en/blog/posts/java/1417/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1424">I found some bugs in Create mod and checked them in Minecraft</a></li><li><a href="https://pvs-studio.com/en/blog/posts/cpp/1422/">Game++. Part 2.2: Types of optimization</a></li><li><a href="https://pvs-studio.com/en/blog/posts/csharp/1421/">What’s new in C# 15: Overview</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=71dacd8fc061" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/unity3d-for-net-developers-how-to-get-started-71dacd8fc061">Unity3D for .NET Developers: How to get started</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What Is PVS-Studio?]]></title>
            <link>https://medium.com/pvs-studio/what-is-pvs-studio-9f6571d9ef30?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/9f6571d9ef30</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[security]]></category>
            <category><![CDATA[static-code-analysis]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[sast]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Mon, 02 Nov 2026 12:33:34 GMT</pubDate>
            <atom:updated>2026-11-02T12:33:34.486Z</atom:updated>
            <content:encoded><![CDATA[<iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fp4Eqcm5y-hA%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dp4Eqcm5y-hA&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fp4Eqcm5y-hA%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/e12b8059cc0ea4a314094d568a62487d/href">https://medium.com/media/e12b8059cc0ea4a314094d568a62487d/href</a></iframe><p><strong>Static Analysis for Better Code</strong></p><p>PVS-Studio is a Static Application Security Testing (SAST) tool that detects issues in source code, from simple typos to potential vulnerabilities. It offers over 1,300 diagnostic rules for C, C++, C#, Java, JavaScript, TypeScript, and Go, including specialized rules for particular technologies and support for standards such as MISRA and AUTOSAR.</p><p>Watch the video to learn more:</p><ul><li>what languages and standards the analyzer supports;</li><li>how it works;</li><li>where it fits into your development workflow;</li><li>and the kinds of bugs it can catch.</li></ul><p><a href="https://pvs-studio.com/what_is_pvs_video_en?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=manual&amp;utm_content=What_is_PVS-Studio">Visit our website to get a free 30-day trial!</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9f6571d9ef30" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/what-is-pvs-studio-9f6571d9ef30">What Is PVS-Studio?</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Pitfalls of bit fields in C++]]></title>
            <link>https://medium.com/pvs-studio/pitfalls-of-bit-fields-in-c-e857556644cb?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/e857556644cb</guid>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[cpp]]></category>
            <category><![CDATA[cpp-programming]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Fri, 30 Oct 2026 13:19:34 GMT</pubDate>
            <atom:updated>2026-11-02T14:00:34.850Z</atom:updated>
            <content:encoded><![CDATA[<p>C++ is a language that lets developers do absolutely anything, and then it just silently watches as they blow everything up. We’re used to thinking in terms of bytes, but what if the standard type system seems too wasteful? When your inner perfectionist demands compactness, bit fields are the best solution. Why use an entire eight-bit container for a simple yes-or-no flag when you can carve out exactly one bit with surgical precision? Add a colon, and suddenly you’re the master of memory. However, when it comes to saving memory, order and predictability end. A programmer is left to deal with the subtleties of standards and the behavior of specific platforms. Let’s take a look under the hood of C++ and see how bit fields actually work.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*dYuDwAOqJ-PABVk2.png" /></figure><h3>Introduction</h3><p>How did I end up here, and why did I even decide to cover this topic?</p><p>For quite some time, I’ve been working on supporting the MISRA C++ 2023 rules. Yes, PVS-Studio analyzer learns how to check whether a codebase complies with this standard. My task was to implement support for a rule that regulates the co-use of enumerations and bit fields. It all looked simple in my head: all I had to do was check whether the enum values with a fixed fundamental data type fit within the specified bit field width.</p><p>Have you ever started a task that seemed like a fun little adventure that would take just a couple of hours, only to find yourself stuck in a bottomless pit of troubleshooting for an entire week? That’s exactly what happened to me. I got bogged down in trying to implement support for this rule.</p><p>As I tried to untangle the mess, I realized one thing: bit fields in C++ are an underrated topic that every C++ developer should know about.</p><h3>The basics: what are bit fields?</h3><p>In C++, a bit field is a data-packing mechanism that allows you to allocate memory within a structure or class bit by bit instead of byte by byte. Unlike ordinary types, which always occupy at least one byte, bit fields enable you to allocate exactly the number of bits you want to a particular variable. This makes them invaluable when memory is strictly limited or when you need to store a set of flags as compactly as possible.</p><p>Let’s take a look at the syntax for declaring bit fields. It may differ slightly depending on your technical requirements, but the fundamental structure is as follows:</p><pre>type-specifier declarator : constant-expression ;</pre><p>Here, constant-expression specifies how many bits to allocate to a particular field.</p><p>The syntax for bit fields is quite interesting. We use a colon to give the compiler a direct instruction to override the usual memory allocation rules. In standard C++ code, the smallest addressable unit is a byte, which contains CHAR_BIT bits. The implementation defines the CHAR_BIT macro, but its most common value is eight. The colon separates the variable&#39;s logical name from its actual size. This forces the compiler to divide memory into small chunks of a specific size.</p><p>This approach saves memory. How? It eliminates empty and redundant bits. When you declare standard variables, the processor rounds their size up to whole bytes due to architectural constraints. Bit fields work around this limitation by packing multiple fields into a shared storage unit.</p><p>To understand how bit fields save space, we’ll compare two ways of storing the same data. Let’s say we need to record three system parameters:</p><ul><li>activity status (on/off): 1 bit is enough (values 0 or 1);</li><li>mode (4 modes): 2 bits are enough (combinations 00, 01, 10, and 11);</li><li>error code (from 0 to 7): 3 bits are enough (combinations from 000 to 111).</li></ul><p>All the essential information about the object state takes up exactly six bits.</p><p><strong>Option 1. Standard structure</strong></p><p>If we create a standard structure in C++, the compiler allocates one byte of memory for each type, since this is the minimum addressable size:</p><pre>struct StandardStruct <br>{<br>  bool is_active;   <br>  char mode;        <br>  char error_code; <br>};</pre><p>The full code:</p><p><a href="https://godbolt.org/z/1s9v7hhc9">The example:</a></p><pre>#include &lt;iostream&gt;</pre><pre>struct StandardStruct <br>{<br>  bool is_active;   <br>  char mode;        <br>  char error_code; <br>}; </pre><pre>int main()<br>{<br>  StandardStruct stStruct;<br>  std::cout &lt;&lt; &quot;Sizeof stStruct: &quot; &lt;&lt; sizeof(stStruct)<br>            &lt;&lt; &quot;byte(s)&quot; &lt;&lt; std::endl;<br>}</pre><p>Program output:</p><pre>Sizeof stStruct: 3 byte(s)</pre><p>The program uses 24 memory bits. 24 memory cells were allocated for our six units of information; the remaining 18 cells simply remain empty because the processor architecture can’t directly handle units smaller than one byte.</p><p><strong>Option 2. Structure using bit fields</strong></p><p>Now let’s add the colon syntax. The compiler starts with a fundamental data type, or container, such as unsigned char, which is one byte (eight bits) in size. It then begins packing fields tightly inside the container:</p><pre>struct BitFieldStruct<br>{<br>  unsigned char is_active  : 1; <br>  unsigned char mode       : 2; <br>  unsigned char error_code : 3; <br>};</pre><p>The full code:</p><p><a href="https://godbolt.org/z/9PqjcvoT8">The example:</a></p><pre>#include &lt;iostream&gt;</pre><pre>struct BitFieldStruct<br>{<br>  unsigned char is_active  : 1; <br>  unsigned char mode       : 2; <br>  unsigned char error_code : 3; <br>};</pre><pre>int main()<br>{<br>  BitFieldStruct stStruct;<br>  std::cout &lt;&lt; &quot;Sizeof stStruct: &quot; &lt;&lt; sizeof(stStruct)<br>            &lt;&lt; &quot; byte(s)&quot; &lt;&lt; std::endl;<br>}</pre><p>Program output:</p><pre>Sizeof stStruct: 1 byte(s)</pre><p>As a result of this mechanism, the entire structure is compressed to one byte. In memory, it appears as a single eight-bit array divided into sectors:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/730/0*aKStsTwNgEcodx0g.png" /></figure><p>The compiler handles everything: when reading from or writing to the mode variable, it automatically generates hidden bit shifts and masks to isolate the required two bits from the rest of the data. This high-density packaging significantly reduces RAM usage in large arrays and embedded systems (microcontrollers).</p><p>How bit fields work and their limitations</p><p>We’ve covered the basic principles and syntax of bit fields. Now let’s take a closer look at how they work and explore the rules imposed by the language.</p><p>Bit fields can exist only inside structures or classes. Developers can’t declare a bit field as a standalone variable inside a function. It’s also impossible to apply the const or volatile qualifiers directly to an individual bit field. Instead, you can apply them indirectly by declaring an object of that structure or class with the relevant qualifier.</p><h3>Data types for bit fields</h3><p>The next thing to discuss is which data types you can use with bit fields. The C++ standard allows this mechanism only for <em>integral</em> types and <em>enumerations</em>. Trying to limit the bit width of floating-point types (float, double) or complex objects such as std::string is technically impossible and will result in a compilation error.</p><p>Bit fields support the following types:</p><p><em>1</em>. Integral types (char, wchar_t, char8_t, char16_t, char32_t, bool, short, int, long, and long long)<em>.</em></p><p>These are the standard options for creating masks and flags. Before C++14, explicitly specifying the signed or unsigned modifier for an int bit field was critical. Otherwise, the compiler determined how to interpret the signed bit. For example, a one-bit int field without an explicit unsigned modifier could take 0 and -1 instead of the expected 0 and 1. For a bool field, allocating exactly one bit is enough.</p><p><em>2. </em>Enumerations (enum and enum class)<em>.</em></p><p>Bit fields work well for storing enum types with a small set of possible values. If an enum has four possible states, allocating two bits is enough.</p><p><em>3. </em>std::byte (<em>C++20).</em></p><p>Starting with <a href="https://timsong-cpp.github.io/cppwp/n5050/cstddef.syn#lib:byte">C++20</a>, std::byte officially joined the list of permitted types. If a structure handles raw data at the hardware level, using std::byte clearly communicates the developer&#39;s intent and prevents accidental arithmetic errors.</p><h3>Rules for working with bit fields</h3><p>The third important topic to cover is the rules governing bit fields. In C++, four key restrictions apply. <strong>Developers cannot</strong>:</p><ul><li>declare an array of bit fields;</li><li>take the address of a bit field;</li><li>create a pointer to a bit field;</li><li>create a reference to a bit field.</li></ul><p>Let’s look at some examples to understand why bit fields follow these rules and what happens when you try to get around them. We’ll start with a simple structure:</p><pre>struct HardwareFlags<br>{<br>  unsigned int mode   : 3;<br>  unsigned int status : 1;<br>};</pre><h4>No taking addresses or creating pointers</h4><p>The rule against taking addresses or creating pointers stems from a fundamental aspect of computer architecture: a bit field has no physical memory address. Modern processor architectures use the byte as the smallest addressable unit of memory (CHAR_BIT bits). A processor simply has no way to address, say, the third bit within a byte.</p><p>For this reason, we can’t use the address-of operator &amp; on a bit field:</p><pre>HardwareFlags flags;<br>auto address = &amp;flags.mode;<br>int* ptr = &amp;flags.mode;     // doesn&#39;t compile</pre><p>Since individual bit segments don’t have unique addresses in a memory cell, it’s physically impossible to obtain a direct pointer to such a variable.</p><h4>No address, no references</h4><p>There’s another restriction that comes from the inability to take a bit field’s address: you can’t bind a standard reference &amp; to a bit field. In C++, references rely on pointers and memory addressing. Without an address, there&#39;s simply nothing for a reference to bind to:</p><pre>HardwareFlags flags;<br>void checkMode(const unsigned int&amp; m);</pre><pre>unsigned int&amp; ref = flags.mode;        // doesn&#39;t compile<br>checkMode(flags.mode);                 // doesn&#39;t compile</pre><h4>Arrays: why pointer arithmetic falls short</h4><p>I’m afraid I have some bad news: you can’t create an array of bit fields either. The reason lies in pointer arithmetic. When you create an array, the compiler guarantees that its elements occupy consecutive memory locations. When you access the i-th element using arr[i], the compiler calculates its address relative to the start of the array via pointer arithmetic.</p><pre>struct BrokenStruct<br>{<br>  unsigned int items[5] : 2; // doesn&#39;t compile<br>};</pre><h3>The anatomy of alignment</h3><p>Although bit fields aim to save memory, C++ doesn’t specify their exact layout within a structure. Compiler developers have considerable freedom in how they pack bits, so a structure’s actual size depends on the target platform. This flexibility is impressive, but to understand why it exists, let’s take a look at <a href="https://timsong-cpp.github.io/cppwp/n5050/class.bit#1">what the standard says</a>:</p><blockquote><em>Allocation of bit-fields within a class object is implementation-defined. Alignment of bit-fields is implementation-defined. Bit-fields are packed into some addressable allocation unit.</em></blockquote><blockquote><strong><em>Note.</em></strong><em> Bit-fields straddle allocation units on some machines and not on others. Bit-fields are assigned right-to-left on some machines, left-to-right on others.</em></blockquote><p>In reality, this means that the layout rules come not from the language, but from the <a href="https://en.wikipedia.org/wiki/Application_binary_interface">Application Binary Interface</a> (ABI) defined for a particular OS. For example, many Unix-like systems use the <a href="https://itanium-cxx-abi.github.io/cxx-abi/abi.html">Itanium ABI</a>, but it also leaves some aspects of bit ordering unspecified. The packing rules can vary depending on the OS and architecture. For example, MSVC on Windows and GCC on Linux may compile the same structure differently.</p><p>In this section, we’ll focus specifically on how bit fields work. I’ll touch on alignment briefly, as I’ve already covered it in a series of articles: the <a href="https://pvs-studio.com/en/blog/posts/cpp/1339/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1423">first part</a> introduces the basic principles, while the <a href="https://pvs-studio.com/en/blog/posts/cpp/1340/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1423">second</a> and <a href="https://pvs-studio.com/en/blog/posts/cpp/1369/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1423">third</a> explore how inheritance and virtual functions affect alignment.</p><h3>How compilers divide memory into allocation units</h3><p>The phrase “Bit fields are packed into some addressable allocation unit” describes the concept of an <a href="https://timsong-cpp.github.io/cppwp/n5050/class.bit#1"><em>allocation unit</em></a>. According to the C++ standard, an allocation unit is an addressable block of hardware memory, such as a byte or machine word, into which bit-fields are packed consecutively. The exact behavior of these units is implementation-defined and depends largely on the platform’s ABI.</p><p>A compiler never allocates memory for individual bits out of thin air. Instead, it starts with the fundamental data type you specify when declaring a field and uses it to determine the size of the physical container. For example, if you use unsigned char, the allocation unit is one byte (8 bits). If you choose unsigned int, it occupies four bytes (32 bits). The compiler takes this entire physical container and starts slicing it into the specified logical segments, one bit at a time, until no space remains. Once the container runs out of room, the compiler allocates a new unit for the next fields.</p><p>Let’s look at how a compiler packs data:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*X_akToBTVHtTaSzy.png" /></figure><p>The packing algorithm</p><ul><li>The compiler receives a list of bit fields declared in a structure.</li><li>It allocates the first container (memory allocation unit) based on the size of the fundamental data type.</li><li>The first bit field occupies the beginning of the newly allocated container.</li><li>The compiler calculates how many free bits remain in the current container.</li><li>It takes the next bit field from the list for analysis.</li><li>It compares the field’s width in bits with the number of free bits remaining.</li><li>If the field fully fits in the remaining space, the compiler places it in the current container, directly next to the previous one.</li><li>After successfully placing the field, the compiler updates the free-bit counter and moves on to the next field.</li><li>If the field’s width exceeds the available space, the compiler must resolve a standard ambiguity.</li><li>The compiler consults the ABI rules for the target architecture. On x86 and ARM, the compiler moves the entire field to a new container to preserve CPU performance. On other architectures, such as some RISC processors, it may split the field into two parts to maximize memory saving, filling the remaining space in the current container.</li><li>The compiler closes the current container, leaving any unused bits empty.</li><li>It opens a new, empty container (allocation unit).</li><li>The field that didn’t fit occupies the beginning of the new container.</li><li>The compiler then recalculates the number of free bits in the new container and repeats these checks for the remaining fields.</li><li>The process is complete once the compiler has assigned all bit fields in the list to their respective containers.</li></ul><p><strong>Note.</strong> The direction in which the compiler fills a container depends on the ABI rules for the target architecture. The order can be from least to most significant bit or vice versa.</p><h3>sizeof bloat: good vs. bad alignment</h3><p>Why are we even going into such detail about the compiler’s step-by-step algorithm? Bit fields can become a ticking time bomb if one doesn’t understand these inner workings. You write code expecting to save memory, but end up with a structure that’s several times larger than normal. Declaring bit fields haphazardly can trigger sizeof bloat. The compiler loses its capability to pack data tightly and starts inserting unused bytes.</p><p>Two classic developer mistakes usually lead to such hidden memory leaks:</p><ul><li>Creating giant fields whose widths exceed the physical capacity of the current memory container.</li><li>Mixing data types indiscriminately, so adjacent fields keep switching between fundamental data types (for example, char, then int, and then char again). This is a critical issue on Windows (Microsoft ABI), where the compiler starts a new allocation unit whenever the type changes, leaving unused padding behind. This limitation doesn&#39;t affect GCC and Clang on Linux and macOS: they ignore changes in base types and continuously pack fields, minimizing alignment padding.</li></ul><p>To see just how much of a difference this makes, let’s compare two scenarios: one that shows how not to do it, and one that shows how to do it right.</p><p><strong>Example N1. Inefficient layout (poor alignment)</strong></p><p>Imagine a structure where we’ve tried to save memory by mixing different <a href="https://godbolt.org/z/Gfohbh69o">types</a>. For this example, let’s use MSVC on Windows:</p><pre>struct BadPacking<br>{<br>  unsigned char a : 4;<br>  unsigned int b  : 3;<br>  unsigned char c : 4;<br>};</pre><p>The structure fields will be arranged as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*q9NiJoMrzncFokaOFSqErg.png" /></figure><p>The constant switching between types (char &gt; int &gt; char) completely disrupts continuous packing. MSVC treats each type change as a command to close the current allocation unit. It stops filling the byte after a, leaves the remaining bits as padding, and aligns the address for the next block. It then opens a 4-byte container for the int b field, which takes three bits, and immediately closes it to open a container for the char type. As a result, large gaps of unused memory remain. The compiler also has to follow alignment rules, so it performs final alignment on the entire structure. Then, sizeof(BadPacking) returns 12 bytes. Clang and GCC, on the other hand, can handle this structure efficiently: they know how to pack these fields as tightly as possible.</p><p><strong>Example N2. Optimized layout (good alignment)</strong></p><p>To fix this, simply follow the golden rule: convert all fields to a single base <a href="https://godbolt.org/z/jzKxvfxKx">type</a> and group them by meaning.</p><pre>struct GoodPacking {<br>    unsigned int b : 3; <br>    unsigned int a : 4;  <br>    unsigned int c : 4;  <br>};</pre><p>Let’s take a look at how the elements are arranged in the structure now:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*dVBQcpcAQBrjdJoSyWhv4Q.png" /></figure><p>The compiler now works with uniform and predictable four-byte blocks. The b field occupies almost the entire first block, while the a and c fields require only eight bits combined, fitting perfectly into the second int block. As a result, sizeof(GoodPacking) returns just eight bytes (exactly two int containers). The data is packed as tightly as possible so the processor can access it without any unnecessary overhead. This gives us a compact structure.</p><h3>Hardware padding: reserving space for unused bits</h3><p>When writing drivers, implementing network protocols, or working with processor registers, you may encounter hardware controllers with gaps in their bit layout. For example, a chip manufacturer may reserve certain bits in a hardware byte for future use, meaning you can’t write or read data to or from them.</p><p>To precisely mirror the register’s physical layout in code, you can use <a href="https://timsong-cpp.github.io/cppwp/n5050/class.bit#2">unnamed bit fields with non-zero widths</a> as placeholders or padding:</p><pre>struct TestController<br>{<br>  unsigned char transmit_ready : 1; <br>  unsigned char                : 3; <br>  unsigned char receive_ready  : 1;<br>  unsigned char                : 3; <br>};</pre><p>However, they still occupy their designated space in memory when the compiler generates the structure. This enables you to map a C++ structure to the microcontroller’s physical memory layout with bit-level precision, without shifting any of the meaningful flags.</p><p>Forcing a packing boundary: why use a zero-width bit field?</p><p>In some cases, such as when working with hardware interfaces, it’s necessary to prevent the compiler from packing fields into the current allocation unit. For example, you may need to ensure that the next data block starts at a new byte or machine word. To do this, you can use an <a href="https://timsong-cpp.github.io/cppwp/n5050/class.bit#2">unnamed zero-width bit field</a>:</p><pre>struct Test<br>{<br>  unsigned char command : 4; <br>  unsigned char         : 0; <br>  unsigned char data    : 4; <br>};</pre><p>A zero-width bit field acts as an artificial boundary. It tells the compiler that the current memory allocation unit is closed. The next field must start in a new allocation unit and can’t use any remaining bits, even if plenty of space remains. One important rule to remember when working with these fields is that you <strong>can’t give a zero-width bit field a name</strong>. It must always remain unnamed.</p><h3>Under the hood: how compilers store bit fields</h3><p>Now, it’s time to take a look under the hood. The internal structure of bit fields in C++ is subject to implementation-defined behavior. The C++ standard abstracts away the physical details, providing only high-level rules and delegating the challenging tasks to the compiler and ABI. To understand how data actually occupies memory, we need to take a closer look at three fundamental factors: bit allocation order, compiler differences across architectures, and binary portability.</p><h3>Bit allocation order</h3><p>The first and most important factor that determines a structure’s physical layout in memory is the order in which the compiler places bits inside an allocated container. Most developers are familiar with byte order, which determines how multibyte values are mapped to memory addresses. A similar concept exists at a lower level, though: the compiler decides which direction to fill individual bits within a particular byte or machine word. The C++ standard deliberately leaves this detail unspecified, so each compiler’s implementation determines how it fills each container. In real world, compilers use one of two opposite approaches: packing from the least significant bit to the most significant bit (LSB-first, Little Endian) or from the most significant bit to the least significant bit (MSB-first, Big Endian).</p><p>In the <a href="https://en.wikipedia.org/wiki/Bit_numbering#Least_significant_bit"><em>LSB-first</em></a> scheme, the compiler fills the allocated container from right to left. In other words, packing begins with the least significant bit, or bit 0. The first bit field declared in the structure occupies the rightmost bits, and each subsequent field sits directly next to it, extending leftward toward bits with greater binary significance. GCC, Clang, and MSVC all use this approach when compiling code for architectures such as x86, x86–64, and ARM.</p><p>The opposite strategy is <a href="https://en.wikipedia.org/wiki/Bit_numbering#Most_significant_bit"><em>MSB-first (Most Significant Bit-first)</em></a>. In this case, the compiler packs bits from left to right, starting with the highest available bit: for example, the seventh bit in a one-byte container. The first field in the structure occupies the most significant bits on the left side of the container, while subsequent fields follow in sequence toward the least significant bits. This is common in compilers for <a href="https://en.wikipedia.org/wiki/Network_processor">network processors</a>.</p><p>Let’s see how this works in real code. Take a look at the following <a href="https://godbolt.org/z/MYM1x8rPK">structure</a>:</p><pre>struct HeaderFlags<br>{<br>  uint8_t version : 3; <br>  uint8_t type    : 2; <br>  uint8_t id      : 3; <br>};</pre><p>The full code:</p><pre>#include &lt;cstdint&gt;<br>#include &lt;cstring&gt;<br>#include &lt;iostream&gt;</pre><pre>struct HeaderFlags<br>{<br>  uint8_t version : 3; <br>  uint8_t type    : 2; <br>  uint8_t id      : 3; <br>};</pre><pre>uint8_t result_byte = 0;</pre><pre>int main()<br>{<br>  HeaderFlags flags;<br>  flags.version = 1;<br>  flags.type = 2;<br>  flags.id = 4;</pre><pre>  std::memcpy(&amp;result_byte, &amp;flags, sizeof(HeaderFlags));<br>    <br>  return 0;<br>}</pre><p>We created a HeaderFlags structure that occupies exactly one byte and assigned three values to its fields. To safely extract the packed byte without violating memory access rules, we used std::memcpy and copied the result into the result_byte global variable.</p><p>Now let’s see how different compilers handle this code. To avoid wading through unnecessary assembly instructions, we’ll enable the -O2 optimization level.</p><p>The <a href="https://godbolt.org/z/6G4s9envG">GCC x86–64</a> output:</p><pre>.globl std::ios_base_library_init()<br>&quot;main&quot;:<br>        mov     BYTE PTR &quot;result_byte&quot;[rip], -111 ; &lt;=<br>        xor     eax, eax<br>        ret<br>&quot;result_byte&quot;:<br>        .zero   1</pre><p>The mov instruction contains the -111 negative value, which is how x86-64 assembly represents one-byte constants. Converting it to the familiar unsigned format gives us 256 - 111 = 145, which is 0x89 in hexadecimal or 0b1001&#39;0001 in binary. The compiler has eliminated all the intermediate packing instructions and written the final result directly to memory. Since the target cell occupies four bytes, the 0x00000089 value appears in memory as the 89 00 00 00 byte sequence. The fact that the least significant byte 89 appears at the lowest memory address confirms the LSB-first (Little Endian) layout.</p><p>Switching the compiler to the Big-Endian MIPS architecture, such as mips-linux-gcc 16.1.0, reveals a completely different picture. The assembly output for this platform <a href="https://godbolt.org/z/99hYqr6b4">looks as follows</a>:</p><pre>.globl std::ios_base_library_init()<br>main:<br>        lui     $2,%hi(result_byte)<br>        li      $3,52                 # 0x34<br>        sb      $3,%lo(result_byte)($2)<br>        jr      $31<br>        move    $2,$0</pre><pre>result_byte:<br>        .space  1</pre><p>Here, the li $3, 52 instruction loads the precomputed constant 52, which is 0x34 in hexadecimal and 0b0011&#39;0100 in binary. The compiler uses the MSB-first strategy, packing bits from left to right: the version field occupies the most significant bits 7–5, type occupies bits 4–3, and id lands in the least significant bits 2–0. We can illustrate the bit layout schematically as follows:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*2km4SSiLqzAzm-Ep.png" /></figure><p>This example shows how changing the bit endianness can completely change the physical representation of data. Directly transferring raw bytes between systems that use different bit-packing schemes can corrupt the data structure. Understanding this mechanism leads us to the second key factor: the architectural differences between compilers and the role of the ABI.</p><h3>Platform dependence: the anatomy of the MSVC vs. GCC/Clang conflict</h3><p>We often think that fundamental data types in C++ behave deterministically on the same hardware architecture. However, bit fields have an important caveat: the compiler decides how to arrange them in memory. The in-memory layout of the same structure compiled for an x86–64 processor can differ significantly between Windows and Linux. The physical layout of bit fields follows the rules defined by the ABI. The ABI specifies how the operating system’s platform expects the compiler to group, align, and divide data into individual fields.</p><p>Several ABI standards define different approaches to data packing. Here are some of the most widely used ones:</p><ul><li><a href="https://refspecs.linuxbase.org/elf/gabi41.pdf">System V ABI</a> (based on the Itanium C++ ABI) is used by default in GCC and Clang on Linux, macOS, and FreeBSD. Although developers originally designed this standard for Intel Itanium processors, it has become a common foundation for GCC and Clang across various architectures, including <a href="https://www.sco.com/developers/devspecs/abi386-4.pdf">x86</a>, <a href="https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf">x64</a>, and <a href="https://github.com/ARM-software/abi-aa/blob/main/sysvabi64/sysvabi64.rst">ARM64</a>.</li><li><a href="https://docs.riscv.org/reference/abi/v1.0/index.html">RISC-V ABI</a> is a specification for the RISC-V open-standard architecture. This standard isn’t formally derived from the System V ABI, but it shares many similar data packing and alignment rules. The goal is to pack bit fields in memory as tightly as possible.</li><li><a href="https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions?view=msvc-170">Microsoft x64 ABI</a> is used by default in MSVC on Windows systems running on the x86–64 architecture.</li><li><a href="https://learn.microsoft.com/en-us/cpp/build/arm64-windows-abi-conventions?view=msvc-170">Microsoft ARM64 ABI</a> is the modern Windows standard for ARM64 processors. It’s designed with x64 limitations in mind. Its structure alignment rules offer greater flexibility, and its calling convention enables programs to pass data directly through registers, reducing stack usage.</li></ul><p>These documents differ in a single rule.</p><p><strong>The rule. Compatibility between different data types</strong></p><p>Developers can declare consecutive bit fields with different fundamental data types within the same structure. This kind of type mixing is one of the main causes of hidden memory bloat, as compilers use very different algorithms to handle boundaries between allocation units with different fundamental types.</p><p>Let’s see what happens when these rules conflict. We’ll run an experiment using Compiler Explorer and measure the structure’s size with the sizeof operator. Let&#39;s write the following structure:</p><pre>struct PackagingConflict<br>{<br>  uint8_t  mode : 4; <br>  uint32_t type : 4; <br>};</pre><p>The full code:</p><pre>#include &lt;iostream&gt;<br>#include &lt;cstdint&gt;</pre><pre>struct PackagingConflict {<br>    uint8_t  mode : 4; <br>    uint32_t type : 4; <br>};</pre><pre>int main() {<br>    std::cout &lt;&lt; &quot;--- BITFIELD PACKING SIZES ---&quot; &lt;&lt; std::endl;<br>    std::cout &lt;&lt; &quot;1. PackagingConflict size: &quot; &lt;&lt; sizeof(PackagingConflict)<br>              &lt;&lt; &quot; byte(s)&quot; &lt;&lt; std::endl;    <br>    return 0;<br>}</pre><p>Look at the outputs from <a href="https://godbolt.org/z/nY8WMGGT7">Clang</a> and <a href="https://godbolt.org/z/qrjj17vcE">MSVC</a>:</p><p>The Clang program output:</p><pre>--- BITFIELD PACKING SIZES ---<br>1. PackagingConflict size: 4 byte(s)</pre><p>The MSVC program output:</p><pre>--- BITFIELD PACKING SIZES ---<br>1. PackagingConflict size: 8 byte(s)</pre><p>Here’s a visual layout of the memory components:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*XWjm0JVT0NuYnfHF.png" /></figure><p>Upon analyzing this structure, we can spot a clear difference: Clang packs it into four bytes, while MSVC inflates it to eight. Since the fields use different fundamental data types (uint8_t and uint32_t), both compilers initially keep them separate. They allocate one byte for the mode and add three bytes of padding so the type starts at a properly aligned four-byte boundary. Clang then applies an Itanium ABI optimization, trimming the unused part of the second allocation unit to save space. MSVC, on the other hand, strictly follows the Microsoft ABI rule that the structure&#39;s total size must be a multiple of its largest fundamental type (uint32_t, or four bytes). It preserves the full four-byte allocation unit for each block, doubling the structure&#39;s final size.</p><p>The real danger is when we move between platforms with different ABIs, the well-defined physical boundaries of data in memory simply disappear. Hidden padding and differences in how compilers handle unused space within allocation units can completely change the internal field layout. This makes bit fields unsuitable for direct byte-by-byte copying, file storage, or binary serialization. If a structure is saved using Clang’s four-byte layout and then read using MSVC’s eight-byte layout, the program reads from incorrect offsets, resulting in corrupted data.</p><h3>Binary fragility and industry standards</h3><p>Due to inconsistencies in alignment and bit ordering, structures containing bit fields offer virtually no binary portability. C++ compilers treat bit fields as a black box: you write concise code, but leave the actual memory layout to chance.</p><p>Using bit fields is virtually guaranteed to cause problems in three areas:</p><p><strong>1. Network I/O.</strong> Sending a structure directly over a socket with a call like send(sock, &amp;packet, sizeof(packet), 0) is a ticking time bomb. If the server runs Linux (Clang) and the client runs Windows (MSVC), ABI differences can shift bit positions, turning valid flags into a meaningless jumble.</p><p><strong>2. File I/O (serialization).</strong> For example, writing bit fields directly to a file byte by byte with fwrite ties the data to a specific ABI. According to Section <a href="https://www.open-std.org/jtc1/sc22/wg14/www/docs/n2310.pdf#page=95">6.7.2.1</a> of the ISO C standard, the order in which compilers lay out bit fields and padding is implementation-defined behavior. Writing such a structure to a file on Linux and then reading it on Windows can cause differences between the Microsoft and System V ABIs to result in the program reading corrupted data from incorrect offsets.</p><p>The C++ object model in C++23 partially addresses the issue of object lifetimes with the <a href="https://en.cppreference.com/cpp/memory/start_lifetime_as">std::start_lifetime_as</a> function. However, this function only validates the creation of an object from raw bytes in memory; it doesn&#39;t address differences in the physical layout of fields across platforms. Moreover, copying uninitialized structure padding when writing data to a file technically involves reading indeterminate values, which leads to undefined behavior.</p><p><strong>3. Drivers and hardware (Hardware MMIO).</strong> A microcontroller’s hardware defines the bit layout of its registers, and it never changes. In C, however, the order and alignment of bit fields, as well as the width of memory accesses, depend on the implementation. So, describing a hardware register with bit fields doesn’t guarantee that each field will map to the intended bits. Switching compilers, compiler versions, or build flags can shift the fields, causing writes to affect the wrong register bits.</p><p>Developers avoid bit fields entirely for any task that requires a strictly defined data layout, such as network communication, file storage, or hardware registers. Instead, they use fixed-width integer types (uint8_t, uint16_t, and uint32_t) combined with explicit bit shifts (&lt;&lt;, &gt;&gt;) and bit masks (&amp;, |).</p><p>Differences between compiler ABIs make it difficult to use standard bit fields without manual alignment for cross-platform data exchange. A program compiled on Linux using GCC or Clang produces a data structure that a Windows application built with MSVC can’t read correctly because the Windows application expects alignment and padding bits in different locations.</p><h3>Use cases: where bit fields actually make sense</h3><p>In C++ development, bit fields serve specific engineering needs rather than abstract memory-saving goals. There are two main scenarios where this feature proves most useful.</p><p><strong>1. Compact metadata for millions of objects.</strong> Developers often need to store metadata, such as status flags, type IDs, and reference counts, for huge numbers of small elements when designing game engines, custom memory allocators, or databases. Allocating a separate variable for each flag can waste enormous amounts of memory due to compiler-imposed alignment. Bit fields let you pack several independent properties into a single byte, minimizing memory usage.</p><p><strong>2. Optimizing CPU cache usage (Cache Lines).</strong> Modern processors don’t read data from RAM one item at a time. Instead, they fetch large blocks of data called cache lines, typically 64 bytes each. When a data structure becomes too large, each cache line contains fewer useful objects. This forces the processor to repeatedly wait for slow RAM access, resulting in a cache miss. Using bit fields to pack data tightly allows the CPU cache to hold several times more objects. An algorithm can run several times faster when it iterates over objects sequentially in large batches because it retrieves data from the CPU’s ultra-fast cache.</p><h3>Modern alternatives to bit fields</h3><p>Due to portability issues, dependence on platforms, and restrictions on address taking, developers often need to look for alternatives to traditional bit fields. Modern C++ offers three main architectural alternatives, each designed to address a specific use case.</p><h3>The classic approach: manual bit masks and shifts</h3><p>If you need to have complete control over the physical position of each bit, the classic C-style approach is your best option. Rather than relying on the compiler to automatically pack bits, you select a fixed-width integer type and manually manipulate individual bits using bitwise operators (&amp;, |, ~, ^, &lt;&lt;, and &gt;&gt;).</p><p>Take a look at the example:</p><pre>class DeviceConfig<br>{<br>public:<br>  void set_mode(uint8_t mode)<br>  {<br>    data = (data &amp; ~MODE_MASK) | ((mode &lt;&lt; 1) &amp; MODE_MASK);<br>  }</pre><pre>  uint8_t get_mode() const<br>  {<br>    return (data &amp; MODE_MASK) &gt;&gt; 1;<br>  }</pre><pre>private:<br>  static constexpr uint8_t ENABLE_CLOCK_MASK = 0x01;<br>  static constexpr uint8_t MODE_MASK         = 0x0E;<br>  static constexpr uint8_t INTERRUPT_MASK    = 0xF0;</pre><pre>private:<br>  uint8_t data = 0;<br>};</pre><p>Yes, this code is more complex, but its binary layout is now completely independent of the compiler and ABI. This makes it an ideal approach for implementing network protocols and writing device drivers. The only thing developers need to keep in mind is the processor’s byte order, or endianness. Multi-byte values appear in reverse order on Big Endian architectures compared to Little Endian ones.</p><h3>For maximum safety: the std::bitset container</h3><p>The built-in <a href="https://timsong-cpp.github.io/cppwp/n5050/bitset">std::bitset</a> class is the best choice if code safety and convenient manipulation of Boolean flags matter more than saving memory in individual structures.</p><p>This container provides a fixed-size array of bits whose size is specified as a template parameter at compile time. It encapsulates all bitwise operations, providing developers with a high-level, type-safe interface.</p><p>Look at the following <a href="https://godbolt.org/z/vh47czvWT">example</a>:</p><pre>#include &lt;bitset&gt;<br>#include &lt;iostream&gt;</pre><pre>enum Permission<br>{<br>  Read = 0,<br>  Write = 1,<br>  Execute = 2,<br>  Admin = 3,<br>  MaxPermissions <br>};</pre><pre>void ManagePermissions()<br>{<br>  std::bitset&lt;MaxPermissions&gt; user_rights;<br>  user_rights.set(Permission::Read);    <br>  user_rights.set(Permission::Write);   <br>  user_rights.flip(Permission::Execute); <br>  <br>  if (user_rights.test(Permission::Write))<br>  {<br>    std::cout &lt;&lt; &quot;The user can write files.\n&quot;;<br>  }</pre><pre>  std::cout &lt;&lt; &quot;Current rights mask: &quot; &lt;&lt; user_rights &lt;&lt; &quot;\n&quot;;<br>}</pre><pre>int main()<br>{<br>  ManagePermissions();<br>}</pre><p>The main advantage of std::bitset is that it protects against human error. If you pass an invalid index to one of its member functions—<a href="https://timsong-cpp.github.io/cppwp/n5050/bitset#lib:set_(member),bitset_">std::bitset&lt;N&gt;::set</a>, <a href="https://timsong-cpp.github.io/cppwp/n5050/bitset#lib:reset,bitset_">std::bitset&lt;N&gt;::reset</a>, <a href="https://timsong-cpp.github.io/cppwp/n5050/bitset#lib:flip,bitset_">std::bitset&lt;N&gt;::flip</a>, or <a href="https://timsong-cpp.github.io/cppwp/n5050/bitset#lib:test,bitset">std::bitset&lt;N&gt;::test</a>—such as index 15 for a four-bit bitset, the function throws a std::out_of_range exception. With manual bit masks, the same mistake could lead to unpredictable reads from unrelated memory or corrupt adjacent data.</p><p>There’s a caveat, though: std::bitset typically rounds its internal storage up to the processor&#39;s native word size, so it can&#39;t save memory at the individual-bit level.</p><h3>For large numbers of flags: the std::vector&lt;bool&gt; specialization</h3><p>Using a regular bool array to manage a huge, dynamically sized array of Boolean flags wastes a lot of memory because each element occupies an entire byte. To fix this issue, the standard library provides a specialized version of std::vector: <a href="https://timsong-cpp.github.io/cppwp/n5050/vector.bool">std::vector&lt;bool&gt;.</a></p><p>This container works differently from ordinary vectors. The standard library optimizes it for memory efficiency by packing bool elements bit by bit into larger integer types (machine words). Usually, it can store 32 or 64 Boolean values in a single memory block.</p><p>The following is an example of using a large dynamic array:</p><pre>#include &lt;vector&gt;</pre><pre>std::vector&lt;bool&gt; feature_flags(10000, false);</pre><pre>void process_features()<br>{<br>  feature_flags[42] = true; <br>  if (feature_flags[42])<br>  {<br>    // code<br>  }<br>}</pre><p>Despite its convenience, std::vector&lt;bool&gt; is one of the most controversial features of the C++ standard. Developers should exercise caution when using it. Because it packs bits so tightly, its elements aren&#39;t actual bool objects in memory. As a result, you can&#39;t take the address of an element directly (the bool *ptr = &amp;feature_flags[0]; code won&#39;t compile) or pass an element by reference or pointer. The [] operator returns a special temporary proxy object that mimics reference behavior rather than a reference. This is the trade-off for compressing data in dynamically allocated memory.</p><h3>Conclusion</h3><p>Well, that’s all I have for you today. What can I say? Bit fields in C++ perfectly embody the language’s philosophy. They give you complete freedom to cut out exactly one bit, then leave you alone to navigate the hidden landmines buried in compilers and system ABIs. Now that you’ve looked under the hood and seen how this intricate memory optimization coexists with platform-dependent chaos, there’s no room for illusions. Bit fields are a powerful tool for optimizing cache lines and working with hardware. However, they can also create hidden issues when you try to send a structure over the network. Now you know the rules of this low-level game. How you wield this power over memory in your own projects is up to you.</p><p><a href="https://pvs-studio.com/en/pvs-studio/try-free/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1423"><strong>Get a free one-month trial</strong></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=e857556644cb" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/pitfalls-of-bit-fields-in-c-e857556644cb">Pitfalls of bit fields in C++</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Game++. Part 2.2: Types of optimization]]></title>
            <link>https://medium.com/pvs-studio/game-part-2-2-types-of-optimization-5d014a3a3088?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/5d014a3a3088</guid>
            <category><![CDATA[game-development]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[cpp]]></category>
            <category><![CDATA[game-engine]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Fri, 23 Oct 2026 12:29:12 GMT</pubDate>
            <atom:updated>2026-10-23T12:29:12.193Z</atom:updated>
            <content:encoded><![CDATA[<p>This book offers insights into C++, including the algorithms and practices used in game development, and explores the language’s strengths and weaknesses, its established workflows, and hands-on solutions, because C++ still dominates the game industry today thanks to its combination of high performance, flexibility, and deep low-level control.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*CcddCVIA4T87wes6.png" /></figure><h3>Hotspots and bottlenecks</h3><p>The key task in optimization is to find hotspots, which are code fragments with a high computational load, and bottlenecks, which are components that limit overall throughput, and these problems usually fall into three categories, namely abstractions, smoothness, and hardware limitations.</p><p>Abstractions, or more precisely premature abstractions, add unnecessary layers to the engine architecture and make the whole system more complex than it needs to be, and the usual suspects here are multiple inheritance, one-size-fits-all solutions to game development problems, and heavy use of design patterns in places where simple procedural code would do the job just fine.</p><p>The same category includes writing beautiful code instead of fast code, when performance-critical sections get buried under unnecessary layers of ceremony and checks, which is exactly what the KISS principle warns us about.</p><p>Most developers agree that smoothness matters more than peak performance, and the things that work against it are well known, because sporadic memory allocations on the main thread make the frame rate uneven, algorithms with unpredictable complexity make frame times inconsistent, and blocking resource loads cause stuttering. A stable 30 FPS beats a frame rate that jumps between 60 and 20, because players feel those jumps immediately, which is why a consistent frame rate is such an easy-to-understand and measurable indicator of quality.</p><p>Hardware limitations are best treated as creative constraints, because brute-forcing a platform’s physical limits instead of applying smart optimizations only adds complexity without any real payoff. Raising the polygon count without a proper level-of-detail system will leave the player with low FPS, and the same goes for complex shaders that ignore fill-rate limits, memory-hungry data structures that don’t account for the cache hierarchy, and so on.</p><p>A modern CPU-GPU architecture is a complex data-processing pipeline with many parallel execution units, and in this “digital hell” data moves through a hierarchy of caches, travels between the CPU and GPU over the bus, and gets synchronized across threads with memory barriers and atomic operations, so the system almost never does exactly what you wrote in the code.</p><p>You can get solid and stable performance by balancing the workload across all of these components, and even then you may not be able to push the CPU beyond 70 to 80%, but you can make much better use of the GPU, memory bandwidth, and caches. The real challenge is finding a delicate balance right at the edge of what’s possible for every component at once, and if you get it wrong, resource contention follows, one component becomes the critical link, and the rest of the system suffers from pipeline stalls, context-switching overhead, and reduced throughput.</p><h3>Hotspots</h3><p>In software systems, hotspots are localized sections of code with unusually high resource consumption, whether that’s CPU time, memory, or disk I/O, and they’re characterized by a disproportionately high execution frequency, heavy CPU utilization, high memory pressure, or significant system call overhead.</p><p>In the context of code optimization, the Pareto principle, also known as the 80/20 rule, means that 20% of the source code accounts for 80% of a program’s total execution time, which makes hunting down hotspots a highly effective strategy. Searching iteratively helps you find the code blocks that consume the most resources so you can target them and cool them down, but once one issue is fixed, the workload shifts and previously minor sections turn into new hotspots that need attention. This process goes on until the results are acceptable or the time allocated for the task runs out, and it’s worth remembering that an engineer’s time is a finite resource too, because the more time a developer spends polishing one part of the code, the less time they have for everything else.</p><p>Modern profilers provide detailed analysis down to the level of individual CPU instructions, which makes it possible to identify temporal hotspots, code with a high number of cache misses, heavy I/O operations, and resource-lock contention in multithreaded applications.</p><h3>Bottlenecks</h3><p>Imagine a dish being prepared in a café. The chef can’t make the whole thing at once, so they hand the side dish off to an assistant while working on the main part themselves, and then the two parts come together and a waiter takes the finished plate to the customer. How long will the whole process take? Suppose the chef finishes first, but the assistant simply can’t work any faster, and now the assistant is the bottleneck that slows down the entire system.</p><p>Games work exactly the same way, because there’s always a “slow assistant” somewhere, whether it’s the CPU, the GPU, memory, or something in the code that holds everything else back, and the developer’s job is to find that weak link and bring its performance in line with the rest. Optimization almost always involves trade-offs, and in 99% of cases there’s no silver bullet that will fix the problem right away.</p><p>The first and most common trade-off is between performance and memory, since sometimes it’s more efficient to store extra data than to recalculate it from scratch. This is called <strong>memoization</strong> or <strong>caching</strong>, and the idea is that you calculate something once, store the result, and then simply pull it from memory whenever you need it, which costs more memory but gets you the result faster.</p><p>The second trade-off is between accuracy and speed. Sometimes you can cut corners and speed up a system by simplifying a data type or using a less precise algorithm, and shaders are a good example, because they provide both log and logp, where the former is more accurate but slower and the latter is faster but less precise. In the final image, players are unlikely to notice that a neighboring polygon has shifted by a quarter of a pixel, but the game may run noticeably faster, and the same principle applies to almost any data model, pathfinding system, or game AI. The key question is how much precision you&#39;re willing to give up, or, in more formal terms, how much you can shift resources from lower-priority computations to higher-priority ones.</p><p>The third classic trade-off is simplicity versus speed, and this one has more to do with people and the team than with hardware. A classic example is a young, talented developer who has just discovered design patterns and responds by building five levels of nested caching in an attempt to optimize the texture-loading system.</p><p>Sure, they’ll get that system built, and it may even run faster thanks to intrusive pointers, custom allocator pools, and a lock-free hash table, but the result is predictable, because nobody, not even the author a month or two later, can understand, debug, or modify that code without risking breaking something. Other developers on the team start avoiding those files, they groan every time they have to touch the system, and code reviews keep getting comments like “What the hell is this? Can we just rewrite it in plain C++?”</p><p>An experienced developer knows that real performance gains don’t come from showing off their knowledge of algorithms, but from understanding the trade-off between how maintainable the code is and how much the optimization actually pays off.</p><p>Yes, you can squeeze out another 5% of performance with custom memory pools, but that will take two weeks to implement and another month to debug all the weird edge cases, so it might be smarter to use a simple linear allocator and trade some memory for time.</p><p>Mature optimization means fixing the truly critical bottlenecks with clean, well-documented solutions that you can easily switch off with conditional compilation if something goes wrong. In the real world, shipping the game matters more than cool algorithms, and having code the team can maintain matters more than having code that looks impressive in a conference talk.</p><h3>Myths and misconceptions</h3><p>Software optimization is inherently endless, because there will always be something left to improve, but it’s important to understand that past a certain point you start hitting diminishing returns, and every additional effort delivers a smaller and less noticeable performance gain.</p><p>On top of that, developers often spend time chasing dead ends that provide no real benefit (but make for great slides and conference talks) and only make the system harder to maintain. The key skill here is knowing when to stop and ask whether further improvements will actually add value or turn into a witch hunt.</p><p>One of the most common mistakes is making decisions based on assumptions. You can’t just “guess” what’s slowing the system down, because those guesses are usually wrong, and the only reliable approach is to measure and analyze, then analyze and measure again, until guesses and gut feelings turn into well-founded engineering decisions.</p><p>Optimization doesn’t start after the code is written, and in fact 80% of performance problems can be eliminated at the architecture design stage, because proper decomposition, a clear object hierarchy, and well-thought-out dependency management help you avoid bottlenecks in the first place. Despite the overhead that comes with languages like C# and Java, a well-structured system often outperforms code written in pure C++ that ignores architectural constraints.</p><p>When it comes to optimization, it’s important to focus on worst-case scenarios rather than average ones. Most developers test their games and applications on powerful workstations, which often gives them a distorted picture, and on weaker hardware the bottlenecks can be completely different, with memory bandwidth or CPU power becoming the limiting factor instead of the GPU.</p><p>To analyze performance properly, developers need to deliberately test on configurations with minimal or weak hardware specs, because those machines are much closer to what a good portion of your players actually experience. Players can tell the difference between 55 and 60 FPS, but they rarely complain about it, while a drop to 20 FPS feels like serious lag even if it only happens once in a while, and that’s why consistent frame times often matter more than a high average frame rate.</p><p>Debug and release builds give fundamentally different results (Pic. 3.1). In debug mode, compiler optimizations are turned off and extra checks are turned on, which can slow the code down several times over, and not evenly. For valid performance profiling, work only with release builds and enable debug symbols or disable optimization for specific sections when you need to, because this lets you strike a balance between traceability and accurate measurements while still giving you a realistic picture of performance.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*VX43IsBy_n_u3bmL.png" /></figure><p><em>Picture 3. 1</em></p><p>That said, debug builds are useful in the early stages of testing, when they can reveal excessive CPU usage or inefficient algorithms before those problems become critical.</p><p>Finally, the key requirement for any benchmark is control over external variables, since poorly designed tests can create the illusion of optimization. For example, the first measurement might be taken in fullscreen mode while the second runs in windowed mode, at a different refresh rate, or at a different resolution, and the frame rate goes up because the environment changed rather than the code, which makes the whole comparison meaningless. To avoid mistakes like this, keep every environment parameter fixed and change only one thing between runs, namely the piece of code you’re optimizing. The best practice is to automate your benchmarks, which removes human error and gives you reproducible results.</p><p>Many people think optimization time should be added to sprints once the game is nearly finished, or, even worse, only when performance problems show up. But ignoring optimization in the early stages of development means risking increasingly tangled processes and unpredictable performance problems at the worst possible moment.</p><p>Another myth is that reactive optimization works well enough on its own. Yes, hundreds of games ship after being optimized at the last minute, but this approach usually ends with cutting features or simplifying mechanics to make up for low FPS. Reactive optimization creates a false sense of control, because fixing problems late in development takes far more time and effort than taking a systematic approach throughout the whole project.</p><p>The next myth is that optimization needs large, dedicated blocks of time, when in reality it’s much more effective to work it in gradually. With practices like performance budgets, you can set limits for FPS, CPU, and GPU usage up front and organize the team’s work around those numbers, and continuous monitoring with incremental code adjustments produces better results than occasional optimization pushes in the final weeks of development.</p><p>There’s also a persistent belief that every optimization is justified. In reality, there are plenty of areas where programmers can sink time without seeing any obvious benefit, so only optimize what actually affects performance and weigh the potential payoff against the time the changes will take. This approach keeps you from wasting effort and keeps the focus on the bottlenecks that are really slowing the game down.</p><p>Finally, there’s a misconception that performance is a single metric, when a game that “feels slow” can mean any combination of low FPS, longer response times, high CPU and GPU load, and frame rate drops. To optimize even one of these metrics effectively, developers first need to understand which ones are critical for their particular project, how they relate to each other, and what’s actually holding the game back.</p><p>A systematic approach like this is the only way to avoid the common misconceptions about optimization. Will anyone actually approve such changes, though? Most likely not, because it’s extremely unlikely that the project architect, tech lead, or CTO will sign off on real architectural changes, and that’s not really the programmer’s call to make anyway, so pushing for it tends to step on the architect’s toes. Besides, changing an existing, working, well-tested system can break the systems around it, and there’s no way to predict when the error will surface, only that it inevitably will. That’s exactly why a developer who just joined the team can’t rewrite half the engine “the right way,” and what’s left for them is code-level and algorithmic tweaks, which can still squeeze out a performance improvement of at least 10%.</p><p>This brings us to the reality that game development forces us to balance our “ideal” technical solutions against the practical constraints of the project. Even a programmer who sees through every optimization myth often has to work within an existing architecture they can’t change and settle for local improvements at the algorithm and code level, but this approach can still deliver tangible results, such as the 10 to 15% performance gains that can make all the difference for a successful release.</p><p>Knowing where your influence ends matters, and so does choosing your battles wisely. Optimization was never just a technical skill, because it’s also a team sport where every change needs to be justified, measured, and agreed upon, and in the end the best optimization is the one players actually notice, not the one that gets you a few nods in a technical chat.</p><h3>Tools</h3><p>Any work aimed at improving performance is wasted if you can’t see what’s actually happening in your code. It’s like trying to repair a watch blindfolded or tune a musical instrument with your ears plugged, because you can turn the screws and pluck the strings all you want, but the result will be purely random.</p><p>In everyday life, we get immediate feedback, so if you trip, you fall, if you take a wrong turn, you may end up at a dead end, and if you add too much salt, you ruin the dish. These signals help us correct course right away and remember not to make the same mistake twice. In programming, however, things work differently, because code can execute millions of times per second, process gigabytes of data, and interact with dozens of system components, while all we see is the end result, say, an FPS counter that refuses to go above 50.</p><p>Modern software, and games in particular, can run to hundreds of thousands of lines of code stacked on top of complex operating systems, graphics drivers, and various libraries that add even more code. Without specialized analysis tools, all you can really do is guess whether the CPU can’t keep up, whether the bottleneck is loading textures from disk, or whether memory is leaking somewhere. Profilers, performance monitors, and debuggers can mislead you too, but you can’t do without them, because they show which functions take the longest to execute, how much memory each part of the program uses, how often the application hits the disk, and where the CPU sits idle waiting for data. Without this information you’re flying blind, and with it the solution often becomes obvious, although measuring and analyzing performance correctly is its own kind of science that sometimes borders on alchemy.</p><p>Any tool that measures a program’s performance will inevitably affect that performance. Some tools are light enough on their feet that they barely slow the program down, and those are called <strong>non-intrusive</strong>, while others can seriously drag down an application’s performance, and those are called <strong>intrusive</strong>.</p><p>How much interference you get depends on how much data you need and how often you need it. A simple FPS counter has virtually no impact on the game, while a detailed analyzer that logs every function call and its execution time can slow things down considerably. Some tools, such as Valgrind, go even further and run the entire program inside a virtual machine to collect highly detailed information about every operation, and you can guess what that does to performance, since the program can easily run ten times slower than usual or worse.</p><p>There are several ways to measure performance, and sampling profilers are the gentlest and simplest of them. They periodically take a snapshot of the program to see which code is running at that moment, and by collecting a series of these snapshots, developers can build a statistical picture of what the program spends most of its time doing.</p><p>A more intrusive approach relies on interception tools that sit between the program and external resources, so PIX, for example, intercepts every call the program makes to the GPU through the DirectX API, while network analyzers peek at all network traffic.</p><p>Getting even more detail means adding special commands and profiling markers to the code to collect data, and this process is called instrumentation.</p><p>Choosing the right tool depends on your specific goals and on how much performance you’re willing to sacrifice for a more detailed analysis. The one rule that really matters is that the tool shouldn’t distort the measurements more than the problem you’re hunting for, since there’s little point in optimizing a function that accounts for only 1% of execution time if the profiler itself slows the program down by 50%. That’s why all three types of tools are often used, each in its own circumstances.</p><h3>Theory and terminology</h3><p><strong>Profilers</strong> is an umbrella term for the performance analysis tools used across most of game and software development, and a profiler’s main job is to capture how the workload breaks down during execution in as much detail as the developer allows.</p><p>The classic use case is measuring CPU time, but modern profilers can track much more, including cache misses, memory allocations, disk access, thread usage, and synchronization. Depending on the level of instrumentation, they can give you either an aggregated view of the workload that shows which modules or systems put the most load on the CPU and where, or a per-frame or per-interval snapshot with the call stack and the exact distribution of work across individual instructions.</p><p>Profilers fall into two categories, general-purpose and specialized. General-purpose profilers are usually the ones built into an IDE or a platform SDK, such as the Visual Studio Profiler, and they give you a broad sense of where performance is leaking. Specialized profilers are low-level tools, such as Sony Razor, Intel VTune, PIX, or Tracy, that collect data from CPU performance counters and OS events and let you analyze code behavior with much greater precision, and only these low-level tools let developers evaluate how specific algorithmic steps or memory access patterns affect the overall performance of a piece of code.</p><p><strong>Total time</strong> shows the cumulative execution time of a function, including all the nested calls it makes to other functions. In theory, it helps identify hotspots in an application and gives a complete picture of how much time the program spends in a particular section of code along with all its dependencies, but in practice it can give a distorted view of performance.</p><p>As part of its optimizations, the compiler may inline functions directly into the code of the calling function, which blurs the execution time of the parent function even though the inlined code is what actually does the work. This metric also doesn’t account for indirect costs, such as cache pollution or data eviction, which can seriously undermine how reliable the measurement is.</p><p>For example, when you analyze functions whose performance depends heavily on their input data, such as sorting an almost-sorted array versus an unsorted one, the profiler will report an average value, which can hide real performance problems that only show up in certain use cases.</p><p><strong>Self time</strong> measures only the time spent executing instructions inside the function itself, excluding the time spent in all the functions it calls. This helps identify the code blocks that actually do the expensive work rather than just handing it off to other functions, and functions with heavy algorithms, loops, or number crunching typically show high self time.</p><p>The ObjectUpdate() function may have a high total time because it calls all the other subsystems responsible for updating the game state, but a low self time because it doesn&#39;t do much work on its own, while a collision-physics function may show significant self time, which tells you that this particular section needs optimization. The problems here are much the same as with the previous metric, because self time can&#39;t reflect a function&#39;s actual impact on the overall performance of the system. A function may have a low self time while triggering many expensive operations through other functions, which creates a false impression of efficiency. On top of that, this metric ignores function call overhead, so if a function makes thousands of small calls, the cost of creating stack frames, passing parameters, and returning values can be significant, or even close to the actual execution time, and still stay invisible in the self-time numbers.</p><p>Another issue is “aggressive optimization,” which is especially common with Clang-based compilers that inline small functions and partially embed their code at the call site. As a result, self-time measurements get distorted, because code that belongs to a different function is counted as self time for the caller (Pic. 3.2).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*BizjlCvzmZjrkoj3.png" /></figure><p><em>Picture 3. 2</em></p><p>The <strong>Calls</strong> metric shows how often a particular piece of code runs during a profiling session, which helps reveal how different functions are actually used in the application. Even if their individual execution times are short, functions with a high call count are good candidates for optimization, because the cumulative overhead of all those calls can be significant.</p><p>Analyzing call counts is one of the first steps in finding inefficient algorithms or unnecessary computations (with one caveat, which is covered in the note below). A function that gets called thousands of times per frame may need a whole range of optimization techniques, and a high call count can also point to recursion, resource leaks, or other architectural problems in the application (Pic. 3.3).</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*gCvERNCPS_zWSf6A.png" /></figure><p>Picture 3.3</p><p>Overall, this is a fairly useful metric, but the call count alone doesn’t show how those calls are distributed over time. A function may be called 100,000 times in a session, but if all those calls happen while a level is loading, they have no impact on gameplay performance, and in that case the profiler will show a high call count without telling you that the function sits idle while frames are being rendered. The call count can also be misleading when it comes to spotting real architectural problems, because a high number of calls may simply be the result of a well-designed architecture with lots of small, specialized functions rather than a sign of inefficiency.</p><blockquote><strong><em>NOTE</em></strong></blockquote><blockquote><em>Modern compilers have gotten very good at inlining, so focusing only on call counts can lead to premature or incorrect conclusions.</em></blockquote><p><strong>Total waste time</strong> tracks how long a function spends with its thread blocked while waiting for resources to become available, I/O operations to complete, or system events to occur (Fig. 3.4). This time doesn’t reflect active execution of the function, but rather delays caused by external factors, and a high waste time can point to indirect performance problems that affect the function’s overall execution time. This metric is usually analyzed when refactoring code or reducing dependencies between functions.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*i49DZjUbfCV4WA9s.png" /></figure><p>Picture 3.4</p><p><strong>Author: Sergei Kushnirenko</strong></p><p>Sergei has over 20-year experience in coding and game development. He graduated from ITMO National Research University and began his career developing software for naval simulators, navigation systems, and network solutions. For the past fifteen years, Sergei has specialized in game development: at Electronic Arts, he worked on optimizing The Sims and SimCity BuildIt, and at Gaijin Entertainment, Sergei headed up the porting of games to the Nintendo Switch and Apple TV platforms. Sergei actively participates in open-source projects, including the ImSpinner library and the Pharaoh (1999) game restoration project.</p><h3>All parts</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/1361/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1422">Game++. Part 1.1: C++, game engines, and architectures</a></li><li><a href="https://pvs-studio.com/en/blog/posts/1375/">Game++. Part 1.2: C++, game engines, and architectures</a></li><li><a href="https://pvs-studio.com/en/blog/posts/1384/">Game++. Part 1.3: C++, game engines, and architectures</a></li><li><a href="https://pvs-studio.com/en/blog/posts/1390/">Game++. Part 1.4: C++, game engines, and architectures</a></li><li><a href="https://pvs-studio.com/en/blog/posts/1397/">Game++. Part 2.1: Types of optimization</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5d014a3a3088" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/game-part-2-2-types-of-optimization-5d014a3a3088">Game++. Part 2.2: Types of optimization</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What’s new in C# 15: Overview]]></title>
            <link>https://medium.com/pvs-studio/whats-new-in-c-15-overview-cbfa0b92fc12?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/cbfa0b92fc12</guid>
            <category><![CDATA[csharp]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[dotnet]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[news]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Tue, 20 Oct 2026 14:09:34 GMT</pubDate>
            <atom:updated>2026-10-20T14:09:34.222Z</atom:updated>
            <content:encoded><![CDATA[<p>A new version of C# is coming soon, so we’re continuing our annual tradition of reviewing its new features. The Microsoft team has prepared several highly anticipated and exciting changes for this release. Let’s take a look at them.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*3gIZfw7Q534AtnG-jrHF1g.png" /></figure><h3>Union types</h3><p>We’ll start with the most straightforward change, union types. They enable developers to combine types without relying on inheritance or interfaces where it&#39;s unnecessary or unavailable. A union is a structure that contains the complete set of supported types.</p><pre>public union TestUnion(int, Version, IEnumerable&lt;string&gt;);</pre><p>In the code above, union is the keyword, while int, Version, and IEnumerable&lt;string&gt; are the set of supported types.</p><p>The union types come in handy when traditional inheritance isn&#39;t an option, as it requires the types to have something in common. However, there may be cases where inheritance is impossible or makes little architectural sense. We might need to handle a specific set of types, including primitives. Normally, we&#39;d have to specify a variable of the object type as input and then cast it to the required types within the method, including logic for handling anything unsupported. That&#39;s error-prone. Using union in this situation would solve these problems, making the code safer and easier to read.</p><p>Given the nature of the new structure, when we use union, the compiler already knows the complete list of types it can contain. So, the default branch is no longer necessary. The change protects us from a switch that misses some types while new subclasses keep falling into the default section, which can cause unpredictable program behavior.</p><p>Let’s look at a more specific example. Before this update, we’d have had to write something like this to define a method for handling API responses:</p><pre>public abstract record ApiResult;</pre><pre>public record ErrorData(string Message);<br>public record Success(string Data) : ApiResult;<br>public record ValidationError(ErrorData Data) : ApiResult;</pre><pre>public string Process(ApiResult result)<br>{<br>  return result switch<br>  {<br>    Success s =&gt; s.Data,<br>    ValidationError s =&gt; s.Data.Message,<br>    _ =&gt; &quot;Unknown result&quot;<br>  };<br>}</pre><p>The ApiResult abstract class, from which specific responses inherit, is declared. So, if a new subclass appears and the developer fails to handle it, executions falls into the default branch. With a union, it works differently:</p><pre>public record ErrorData(string Message);<br>public record Success(string Data);<br>public record ValidationError(ErrorData Data);</pre><pre>public union ApiResult(Success, ValidationError)<br>{ <br>  public string GetData()<br>  {<br>    return this switch<br>    {<br>      Success s =&gt; s.Data,<br>      ValidationError s =&gt; s.Data.Message<br>    };<br>  }<br>}</pre><p>As you can see, there’s no need for a parent class just to group the types together. The behavior is also more predictable because the compiler knows at compile time exactly which types union can contain. So, if a switch doesn&#39;t handle every type in the union, the compiler issues a warning.</p><p>Moreover, the union gives developers an easier way to express patterns such as monads. It also enables implementing custom methods and properties within union, so working with all its types doesn&#39;t require writing switch.</p><h3>Closed hierarchies</h3><p>This change is somewhat related to union in that it also lets the compiler know exactly how many types it&#39;ll work with. It does so by preventing inheritance from a class outside the assembly where it was declared. This behavior resembles a mix of internal and sealed: it takes the scope from internal and the restriction on inheritance from sealed. This can be useful, for example, in libraries where we want to restrict inheritance from certain classes without blocking it within our own assembly. Here&#39;s an example declaration:</p><pre>public closed record ClosedRecordClass;</pre><p>The closed keyword marks the class as closed to inheritance. As a result, attempting to inherit from such a class in another assembly causes a compilation error. Note that, like union, a closed hierarchy supports an exhaustive switch, which enables omitting the default branch.</p><p>A few important details are worth keeping in mind. Firstly, a class marked as closed is always abstract. This also helps enable an exhaustive switch. Secondly, descendants don&#39;t automatically become closed. If we need to hide them from other assemblies, we can mark them with the internal access modifier. Thirdly, only classes (class and record class) can be closed to inheritance from another assembly. Structs don&#39;t support inheritance, while interfaces are specifically designed to be inherited from. So, adding the keyword to a struct or an interface will result in a compilation error.</p><h3>Labeled break and continue</h3><p>A small but quite important addition: most of us have probably run into situations where we needed to interrupt the execution of an external structure or skip to its next iteration. The classic approach would have looked something like this:</p><pre>public enum WorkItemType<br>{<br>  Process,<br>  StopAll<br>}<br>public class WorkItem<br>{<br>  // ....<br>  public WorkItemType Type { get; set; }<br>}</pre><pre>public class OldLabeledBreak<br>{<br>  public void PerformAction(IEnumerable&lt;WorkItem&gt; workList)<br>  {<br>    bool stopProcessing = false;</pre><pre>    foreach (var item in workList) // #1<br>    {<br>      switch (item.Type) // #2<br>      {<br>        case WorkItemType.Process:<br>          // ....<br>          break; // break #2<br>        case WorkItemType.StopAll:<br>          stopProcessing = true;<br>          break; // break #2<br>      }</pre><pre>      if (stopProcessing)<br>        break; // break #1<br>    }<br>    // ....<br>  }<br>}</pre><p>In other words, we need an additional flag so that the outer loop can exit after the inner switch finishes executing. Of course, we could use goto, but it can make the code harder to follow because the label code execution jumps to can appear anywhere. Labeled break and continue simplify this. Let&#39;s see what this method would look like now.</p><pre>public void PerformAction(IEnumerable&lt;WorkItem&gt; workList)<br>{<br>  outer:  foreach (var item in workList) // #1<br>  {<br>    switch (item.Type) // #2<br>    {<br>      case WorkItemType.Process:<br>        // ....<br>        break; // break #2<br>      case WorkItemType.StopAll:<br>        break outer; // break #1<br>    }<br>  }<br>  // ....<br>}</pre><p>As you can see, the syntax is similar to goto, while the code becomes more readable. This lets us avoid creating extra flags and checks, which can slightly reduce memory usage and also make the code more expressive. Labels can be applied with the following constructs: for, foreach, while, do, and switch.</p><h3>Extension indexers</h3><p>This change enables developers to define how collection elements can be accessed by index using extension indexers. For example, it’s possible to implement element access with Range for IReadOnlyList. Before C# 15, we could implement this as follows:</p><pre>public class OldExtensionIndexers<br>{<br>  public void Process()<br>  {<br>    IReadOnlyList&lt;string&gt; readOnlyList = [&quot;val1&quot;, &quot;val2&quot;, &quot;val3&quot;];<br>    var elem = readOnlyList.Slice(..2);<br>  }<br>}<br>public static class OldReadOnlyListExtensions<br>{<br>  public static IReadOnlyList&lt;T&gt; Slice&lt;T&gt;(this IReadOnlyList&lt;T&gt; list, <br>                                          Range range)<br>  {<br>    var (offset, length) = range.GetOffsetAndLength(list.Count);<br>    var result = new List&lt;T&gt;(length);<br>    for (int i = 0; i &lt; length; i++)<br>      result.Add(list[offset + i]);<br>    <br>    return result;<br>  }<br>}</pre><p>While a regular extension method already solves this problem, C# 15 introduces the option to create extension indexers. So, we’ll get access to elements using the familiar brackets. Let’s look at how this would work in our example:</p><pre>public class NewExtensionIndexers<br>{<br>  public void Process()<br>  {<br>    IReadOnlyList&lt;string&gt; readOnlyList = [&quot;val1&quot;, &quot;val2&quot;, &quot;val3&quot;];<br>    var elem = readOnlyList[..2];<br>  }<br>}<br>public static class NewReadOnlyListExtensions<br>{<br>  extension&lt;T&gt;(IReadOnlyList&lt;T&gt; list)<br>  {<br>    public IReadOnlyList&lt;T&gt; this[Range range]<br>    {<br>      get<br>      {<br>        var (offset, length) = range.GetOffsetAndLength(list.Count);<br>        return list.Skip(offset).Take(length).ToList();<br>      }<br>    }<br>  }<br>}</pre><p>It’s worth noting that if a type has its own indexer, it takes precedence over an indexer declared as an extension. The official order of precedence is as follows: explicit type indexers, implicit type indexers, explicit extension indexers, and implicit extension indexers.</p><h3>Collection expression arguments</h3><p>The next feature will help shorten code and make collection initialization more convenient by passing the required arguments directly to the constructor. For example, we can pass capacity and comparer. In C# 14 and earlier, we would need to write something like this:</p><pre>public void ProcessList()<br>{<br>  List&lt;string&gt; loginList = [&quot;login1&quot;, &quot;login2&quot;];</pre><pre>  List&lt;string&gt; newLoginList = new List&lt;string&gt;(loginList.Count);<br>  newLoginList.AddRange(loginList);<br>}<br>public void ProcessHashSet()<br>{<br>  HashSet&lt;string&gt; hashSet = new HashSet&lt;string&gt;<br>(StringComparer.OrdinalIgnoreCase);<br>  hashSet.Add(&quot;value 1&quot;);<br>  hashSet.Add(&quot;value 2&quot;);<br>}</pre><p>The initializer supports only one option because List has just three constructors: an empty one, one that takes capacity, and one that takes a collection. Let&#39;s look at the new possible implementation:</p><pre>public void ProcessList()<br>{<br>  List&lt;string&gt; loginList = [&quot;login1&quot;, &quot;login2&quot;];</pre><pre>  List&lt;string&gt; newLoginList = [with(loginList.Count), ..loginList];<br>  // newLoginList.AddRange(loginList);<br>}<br>public void ProcessHashSet()<br>{<br>  HashSet&lt;string&gt; hashSet = [with(StringComparer.OrdinalIgnoreCase), <br>    &quot;value 1&quot;, &quot;value 2&quot;];<br>  // hashSet.Add(&quot;value 1&quot;);<br>  // hashSet.Add(&quot;value 2&quot;);</pre><pre>}</pre><p>The code looks cleaner, right? Your IDE will probably even suggest this syntax and collapse everything into a single line. However, note that with must always come first; otherwise, the compiler will issue an error.</p><h3>Memory safety</h3><p>The final change involves reevaluating what C# considers unsafe. The language team aims to flag only code that actually accesses unmanaged memory as unsafe. In other words, using pointers will no longer automatically make code unsafe. Before C# 15, unsafe code was marked like this:</p><pre>public unsafe class OldMemorySafety<br>{<br>  public int ReadMemory(byte* ptr)<br>  {<br>    return *ptr;<br>  }<br>  public int Process()<br>  {<br>    byte b = 42;<br>    byte* newPtr = &amp;b;<br>    return ReadMemory(newPtr);<br>  }<br>}</pre><p>Any use of pointers used to require the unsafe keyword. To better understand the change, let&#39;s take a look at what the code will look like in the new version:</p><pre>public class NewMemorySafety<br>{<br>  public int ReadMemory(byte* ptr)<br>  {<br>    unsafe<br>    {<br>      return *ptr;<br>    }<br>  }<br>  public int Process()<br>  {<br>    byte b = 42;<br>    byte* newPtr = &amp;b;<br>    return ReadMemory(newPtr);<br>  }<br>}</pre><p>We no longer need to mark the class as unsafe. Instead, the keyword goes only where it&#39;s needed. In this case, dereferencing the pointer is unsafe, so it should be marked as such.</p><p>The safe keyword has also been added. It should be used when a block contains unsafe operations that are safe for the caller&#39;s context, so callers don&#39;t require the unsafe block. In other words, use safe when the compiler can&#39;t verify safety on its own. Examples include extern methods and fields in structs with explicit memory layout ([StructLayout(LayoutKind.Explicit)]).</p><p>It’s also worth mentioning stackalloc. Its use is now considered unsafe when all of the following apply: it&#39;s converted to Span&lt;T&gt; or ReadOnlySpan&lt;T&gt;, it has no initializer, and it&#39;s used where SkipLocalsInitAttribute is present.</p><p>The new rules introduced by this change should be enabled separately. If you’d like to learn more, follow <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/compiler-options/language#enable-the-updated-memory-safety-rules">this link</a> for the official instructions.</p><h3>Conclusion</h3><p>This year’s language release has brought developers a number of fairly important features. Among them are useful additions such as union types and closed hierarchies, while several other changes simplify common coding patterns, making development little smoother once again. Some of these features may not become as popular, but that doesn&#39;t make them any less useful to certain groups of developers. Which of these changes did you find most useful? Share your thoughts in the comments!</p><p>You can read the C# 15 documentation via <a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-15">this link</a>. Don’t forget to check out our overviews of previous versions, here’s the list:</p><ul><li><a href="https://pvs-studio.com/en/blog/posts/csharp/0860/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1421">What’s new in C# 9: overview</a></li><li><a href="https://pvs-studio.com/en/blog/posts/csharp/0875/">What’s new in C# 10: overview</a></li><li><a href="https://pvs-studio.com/en/blog/posts/csharp/1002/">What’s new in C# 11: overview</a></li><li><a href="https://pvs-studio.com/en/blog/posts/csharp/1074/">What’s new in C# 12: overview</a></li><li><a href="https://pvs-studio.com/en/blog/posts/csharp/1173/">What’s new in C# 13: overview</a></li><li><a href="https://pvs-studio.com/en/blog/posts/csharp/1301/">What’s new in C# 14: overview</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=cbfa0b92fc12" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/whats-new-in-c-15-overview-cbfa0b92fc12">What’s new in C# 15: Overview</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[PVS-Studio 8.01: OWASP Benchmark, JDK 25 and Go 1.27 support, and much more]]></title>
            <link>https://medium.com/pvs-studio/pvs-studio-8-01-owasp-benchmark-jdk-25-and-go-1-27-support-and-much-more-c6a7056ab088?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/c6a7056ab088</guid>
            <category><![CDATA[news]]></category>
            <category><![CDATA[cpp]]></category>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[java8]]></category>
            <category><![CDATA[press-release]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Fri, 16 Oct 2026 14:07:29 GMT</pubDate>
            <atom:updated>2026-10-16T14:07:29.811Z</atom:updated>
            <content:encoded><![CDATA[<p>PVS-Studio 8.01 has been released. This version features expanded support for OWASP, JDK 25 and Go 1.27 support, and much more.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PkmgiHt8WmOYZ_BGmMPaKw.png" /></figure><h3>OWASP Benchmark for Java</h3><p>In this release, we checked the Java analyzer SAST rules against the <a href="https://owasp.org/www-project-benchmark/">OWASP Benchmark for Java</a>.</p><p>Evaluating the quality of security-related diagnostic rules on real-world projects can be quite challenging. We can’t predict how many vulnerabilities the code will contain, so we can’t determine which defects the analyzer has overlooked.</p><p>We used the benchmark to address gaps in the Java analyzer SAST capabilities, which made the tool more effective at detecting security defects.</p><h3>New versions of Java and Go</h3><p>With the release of PVS-Studio 8.01, we’ve added support for the latest platform versions. The Java analyzer now supports JDK 25, and the Go analyzer supports Go 1.27.</p><p>This enables teams to analyze projects using the latest language versions and standard libraries without delaying updates due to limitations in their analysis tools.</p><p>JDK 25 is also an LTS release, making its support particularly important for teams migrating to a new long-term support version.</p><h3>Reports with analysis configuration details</h3><p>The PVS-Studio_Cmd.exe utility for analyzing MSBuild projects now includes in JSON reports the project configurations used during analysis. This makes it easier to fix warnings that occur on specific platforms.</p><p>In addition, the fullhtml report generated by <a href="https://pvs-studio.com/en/docs/manual/0038/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">PlogConverter.exe</a> now groups warnings by analysis configuration.</p><h3>Expanded configuration options</h3><p>This release expands the configuration options for our analyzers and utilities, making them easier to tailor to specific project requirements and environments.</p><p>The JavaScript and TypeScript analyzer now supports the max-old-space-size setting. You can find it in the <a href="https://pvs-studio.com/en/docs/manual/7193/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">pvs-settings.toml configuration file</a> and the <a href="https://pvs-studio.com/en/docs/manual/7195/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">pvs-js.exe command-line utility</a>. This setting enables you to specify how much memory the analyzer&#39;s Node.js server can use.</p><p>In PVS-Studio_Cmd.exe, you can now specify dependency cache paths relative to the <a href="https://pvs-studio.com/en/docs/manual/6630/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">.pvsconfig configuration file</a>.</p><p>The C and C++ analyzer now offers more options for configuring diagnostic rules. Now, you can specify custom types as “cheap-to-copy,” and configure the analyzer behavior based on the target platform.</p><p>The <a href="https://pvs-studio.com/en/docs/warnings/v719/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">V719</a> diagnostic rule now supports additional configuration options: ignoring default branches and small enum types, specifying individual values to ignore, and setting the maximum number of constants that can be ignored.</p><h3>Extended warning filtering</h3><p>The <a href="https://pvs-studio.com/en/docs/manual/0038/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">Plog Converter utility</a> for converting and filtering reports now allows you to filter diagnostic rules by CWE codes and SAST identifiers.</p><h3>Breaking Change</h3><p>This release includes a change that breaks backward compatibility with previous versions of the analyzer. You may need to adjust how you use the analyzer due to these changes.</p><p>The PVS-Studio installer for Windows now <a href="https://pvs-studio.com/en/docs/manual/0025/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">returns exit code </a><a href="https://pvs-studio.com/en/docs/manual/0025/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">100</a> if Visual Studio plugin installation fails. We also added the skipVSIXInstallErrorCode flag to disable the non-zero exit code in this case.</p><h3>New diagnostic rules</h3><h3>Java</h3><ul><li><a href="https://pvs-studio.com/en/docs/warnings/v5341/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">V5341</a>. OWASP. A cookie was created without enabling the ‘Secure’ attribute. This may lead to the exposure of sensitive data.</li><li><a href="https://pvs-studio.com/en/docs/warnings/v5342/">V5342</a>. OWASP. Potentially tainted data is stored in an object that can be used as a source of trusted data.</li><li><a href="https://pvs-studio.com/en/docs/warnings/v5343/">V5343</a>. OWASP. A cryptographically weak random number is used for sensitive data.</li></ul><h3>Articles</h3><p>Here’s the traditional roundup of posts from our blog! Over the past two months, we’ve published articles where we looked for bugs in the popular C# ORMs, RepoDB and SqlSugar, reviewed VSCode source code, explored the new features of Java 27, found bugs in the Create mode, and much more. The full list is below:</p><h3>C and C++</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/cpp/1409/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">The worst programming language of all time /s</a></li></ul><h3>C#</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/csharp/1411/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">What popular ORMs for C# hide: Let’s check RepoDB and SqlSugar</a></li></ul><h3>Java</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/java/1412/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">What’s new in Java 27</a></li><li><a href="https://pvs-studio.com/en/blog/posts/java/1417/">I found some bugs in Create mod and checked them in Minecraft</a></li></ul><h3>JavaScript and TypeScript</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/js/1407/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">We checked the source code of VS Code</a></li></ul><h3>Go</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/go/1413/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">PVS-Studio for Go: Quick overview of new diagnostic rules</a></li></ul><h3>Security</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/1408/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">Avoid adding new library to project: 10-year retrospective</a></li></ul><h3>Other</h3><ul><li><a href="https://pvs-studio.com/en/blog/posts/1410/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">Why do we integrate PVS-Studio analyzer with so many tools?</a></li></ul><h3>Webinars</h3><h3>Let’s make a programming language. Variables</h3><p>We continue building our own programming language from scratch. In this video, we’ll take the next step by adding variables to the language.</p><p>So far, our programs have consisted of a single expression built from smaller language constructs. Now it’s time to evolve the language. Instead of parsing a single expression, we’ll introduce a list of declarations, allowing our language to support variable declarations and laying the groundwork for more complex programs.</p><p>During the video, Yuri will walk through the implementation step by step, showing how variables are represented and processed in our language, and how to add this functionality to your language in C++.</p><p>This video is designed for developers who want to understand how programming languages work under the hood — not just how to use them.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FSn8fVb_O90Y%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DSn8fVb_O90Y&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FSn8fVb_O90Y%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/1871b6d56345944b3739cbbf1e8f9f48/href">https://medium.com/media/1871b6d56345944b3739cbbf1e8f9f48/href</a></iframe><h3>Go-Go-Gadg…Error? A closer look at the mistakes Go developers make!</h3><p>Mistakes happen in Go code just like in any other language: typos, duplicates, broken condition checks, and so on.</p><p>But why do they still slip through? After all, many teams use tools like go vet, which catches all of this perfectly well… right?</p><p>In this webinar we covered:</p><ul><li>Common mistakes in Go code, straight from well-known projects;</li><li>Errors that standard tools and approaches fail to catch, and how to handle them.</li></ul><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FgdF_AObnq6Y%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DgdF_AObnq6Y&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FgdF_AObnq6Y%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/baeb48ae59920f0e4240871e43a89759/href">https://medium.com/media/baeb48ae59920f0e4240871e43a89759/href</a></iframe><h3>Let’s make a programming language. Functions</h3><p>We continue building our own programming language from scratch.</p><p>In the previous session, we introduced variables — they could be declared, reference one another, and were resolved through a global hash table. Now it’s time to add one of the core building blocks of any language: functions. Implementing functions is really a story about scopes and name resolution. We’ll explore how identifiers are looked up across different scope levels: the global scope, function scopes with their own variables, and nested local scopes inside function bodies. What happens when the same variable name exists in multiple scopes? How does the compiler determine which one to use?</p><p>In this video, we will walk through the implementation step by step, demonstrating how functions, scopes, and symbol lookup are built in code using C++. Along the way, you’ll gain a deeper understanding of one of the fundamental mechanisms behind modern programming languages.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FQT7tyIENfZI%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DQT7tyIENfZI&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FQT7tyIENfZI%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/6a2cf894364c5dc70428e767b3cba98d/href">https://medium.com/media/6a2cf894364c5dc70428e767b3cba98d/href</a></iframe><h4>Let’s make a programming language. Evaluator</h4><p>We’re reaching the final stage of our journey of building a programming language in C++!</p><p>So far, we’ve created a lexer, a parser, and an AST. Now it’s time to make our language do what it was designed to do — evaluate expressions.</p><p>In this webinar, we’ll explore how to represent the results of computations in our language and learn how to evaluate expressions step by step. By the end of the session, we’ll bring all the pieces together and complete our own programming language.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2F2-3ytWWhy8s%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3D2-3ytWWhy8s&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2F2-3ytWWhy8s%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/04fee14d9380234aceea84ff14e0972d/href">https://medium.com/media/04fee14d9380234aceea84ff14e0972d/href</a></iframe><h3>Go vet can’t go: How to create your own Go analyzer?</h3><p>A static analyzer is a powerful dev tool, and any Go developer who has worked with go vet knows it. But what if its capabilities aren’t enough? Would you look for a new tool, or… build your own?</p><p>In this live talk, PVS-Studio’s Developer Advocate walks through Go’s built-in tools and code-parsing capabilities, explains the basics of static analysis, unpacks what a semantic model is, shows how to build a syntax tree, and much more.</p><p>The end goal? Build your own static analyzer, tailored to catch what matters most in your projects.</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2FKAaJT_mUijA%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DKAaJT_mUijA&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2FKAaJT_mUijA%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/c4e3ad2829be38877d85a0fc58ae9762/href">https://medium.com/media/c4e3ad2829be38877d85a0fc58ae9762/href</a></iframe><h3>Bugbusters</h3><p>This is the first episode of our new podcast, Bugbusters. Our guest is Nathan Baggs, a content creator and YouTuber. We discussed whether it’s worth learning C++ in 2026, how the language is evolving, what it’s used for, and what its drawbacks are. We hope you enjoy this episode! We look forward to your comments!</p><iframe src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Fkd-5hJHvKIY%3Ffeature%3Doembed&amp;display_name=YouTube&amp;url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dkd-5hJHvKIY&amp;image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Fkd-5hJHvKIY%2Fhqdefault.jpg&amp;type=text%2Fhtml&amp;schema=youtube" width="854" height="480" frameborder="0" scrolling="no"><a href="https://medium.com/media/5959cab7c722e807b865b8da6c31707f/href">https://medium.com/media/5959cab7c722e807b865b8da6c31707f/href</a></iframe><h3>Conclusion</h3><p>If you would like to receive news about PVS-Studio releases, <a href="https://pvs-studio.com/en/subscribe/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">subscribe to our newsletter here</a>.</p><p>If you haven’t checked your project using PVS-Studio yet, <a href="https://pvs-studio.com/en/pvs-studio/try-free/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1420">start with a trial license</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c6a7056ab088" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/pvs-studio-8-01-owasp-benchmark-jdk-25-and-go-1-27-support-and-much-more-c6a7056ab088">PVS-Studio 8.01: OWASP Benchmark, JDK 25 and Go 1.27 support, and much more</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[When copy-paste goes wrong: Bugs in structurally isomorphic Go code]]></title>
            <link>https://medium.com/pvs-studio/when-copy-paste-goes-wrong-bugs-in-structurally-isomorphic-go-code-8e43a804304a?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/8e43a804304a</guid>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <category><![CDATA[golang]]></category>
            <category><![CDATA[debugging]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Tue, 13 Oct 2026 14:33:33 GMT</pubDate>
            <atom:updated>2026-10-13T14:33:33.369Z</atom:updated>
            <content:encoded><![CDATA[<p>You might’ve done this many times: you found code in a project that seemed like a good fit for your task, copied it, replaced the variables or function calls with the ones you needed, and then got behavior you didn’t expect when you ran the code. It’s likely that you missed something while making those changes. This is exactly how the errors described in this article occur.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*SYj4j_9CCyWmYiCP26wpYA.png" /></figure><h3>Introduction</h3><p>PVS-Studio 8.00 was recently released, introducing several <a href="https://pvs-studio.com/en/blog/posts/1406/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1419">new major features</a>, including the Go analyzer that comes with several new and cool diagnostic rules.</p><p>It includes a rule that detects one of the trickiest types of copy-paste bugs, the kind that can easily slip past a regular code review. Okay, now let’s take a look at the code:</p><pre>if process.ProcessName != nil &amp;&amp; <br>   (len(*process.ProcessName) == 0 ||<br>   len(*process.ProcessName) &gt; common.NameFieldMaxLength) {</pre><pre>  return kit.CCError.CCErrorf(common.CCErrCommParamsInvalid,        <br>                              common.BKProcessNameField)<br>}<br>....<br>if process.FuncName != nil &amp;&amp; <br>   (len(*process.FuncName) == 0 ||<br>   len(*process.ProcessName) &gt; common.NameFieldMaxLength) {</pre><pre>  return kit.CCError.CCErrorf(common.CCErrCommParamsInvalid, <br>                              common.BKFuncName)<br>}</pre><p>You’ve probably noticed that the expressions in both if statements consist of the same checks, but they aren&#39;t completely identical: the first one uses process.ProcessName, while the second one uses process.FuncName in the same positions. Or... does it?</p><p>The last checks suspiciously break this pattern: they’re exactly the same, and both use the process.ProcessName field. Based on the logic the code has consistently followed so far, the check in the second if should have used a different field, process.FuncName.</p><p>Since such typos can easily slip past manual review, the best solution is to delegate the task of catching them to a static analyzer. But how can we teach a program to compare similar yet distinct code fragments and use those comparisons to detect bugs the way humans do? Is AI necessary for that? In the rest of this article, we’ll explore these questions by looking at how the diagnostic rule works in PVS-Studio for Go.</p><h3>How does the analyzer find these bugs?</h3><p>Modern AI models can indeed detect such bugs. However, integrating artificial intelligence into a static analyzer for such a purpose is too expensive and resource-intensive.</p><p>Fortunately, we don’t need AI here. We can handle the task with a relatively simple heuristic algorithm. We’ll take a closer look at how it works later, but first, let’s go over a few basic concepts from static code analysis:</p><ul><li>Statements are the elements of code that form its skeleton and describe the execution flow (if, switch, for, and {...} blocks).</li><li>Expressions are code elements that manage data (function calls, variables, and operators).</li><li>Nodes is a general term for statements, expressions, and other types of code elements.</li><li>Abstract Syntax Tree (AST) represents code as a hierarchical structure where some elements, like folders in a file system, contain other elements. An AST contains the same information you can get from the code itself, but its structured format makes it better suited for program analysis.</li></ul><p>If you’re interested in delving deeper into static analysis, we have the perfect <a href="https://pvs-studio.com/en/blog/posts/go/1329/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1419">article</a> for you.</p><p>Well, now that we have enough context, we’re ready to face the boss algorithm. Here’s how the <a href="https://pvs-studio.com/en/docs/warnings/v8033/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1419">V8033</a> diagnostic rule in PVS-Studio’s Go analyzer works:</p><ul><li>Within a function, the analyzer takes individual statements and groups of statements, separated by blank lines (hereinafter referred to as code fragments) and builds a simplified AST for them. Unlike a regular AST, it contains only the information needed to describe the code structure and omits specific names of variables, functions, and other semantic information.</li><li>The fragments are grouped by their simplified AST. This is how fragments with similar structure are identified, even though they may still differ in their details (for example, in the variables they use).</li><li>Within each group, the fragments are paired and compared with one another. Let’s look at how the algorithm works for a single pair. For convenience, let’s call the two fragments in one pair A and B.</li><li>The analyzer identifies the variables that appear in both A and B (common variables) as well as the variables that appear in only one of the fragments (specific variables).</li><li>Let’s assume that A is higher in the code than B. If one of the common variables occurs less frequently in B than in A, but still at the same relative positions, the uses of that variable in B are treated as potential typos.</li><li>Next, a replacement candidate is sought among the B variables. This variable must be used in the same relative position as the previously considered general variable in A, but not in B.</li><li>If the analyzer finds such a candidate, it performs a series of final checks to determine whether the variables are interchangeable. If all the checks pass, the developer receives warnings about possible typos in the code.</li></ul><p>Of course, this is just a conceptual outline. A significant amount of important logic remains behind the scenes, especially the logic that filters out false positives. Without it, the diagnostic rule would be far less useful.</p><blockquote><em>It is only 5% of the task to create the diagnostic. It’s not hard for the analyzer to complain about suspicious code, it’s much harder to not complain about the correct code. We spend 95% of our time “teaching” the analyzer to detect various programming techniques, which may seem suspicious for the diagnostic, but in reality they are correct.</em></blockquote><p>From the <a href="https://pvs-studio.com/en/blog/posts/cpp/0466/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1419">article</a> “How PVS-Studio does the bug search: methods and technologies”</p><p>Now that we’ve taken a quick look at how the analyzer works, let’s exercise in looking for similar errors on our own.</p><h3>Find an error</h3><p>I invite you to challenge yourselves by finding errors in this code, which was taken from real open-source projects. The closer you get to the end, the harder the examples become.</p><p>Are you up for the challenge? Let’s get started, then!</p><h3>Yao Agents</h3><p><a href="https://github.com/YaoApp/yao">Yao Agents</a> is an open-source platform for creating and running autonomous AI agents. Its key feature is that the agents run directly on users’ devices, while all operations are centralized and controlled via a single interface.</p><p>We found this code in the <a href="https://github.com/YaoApp/yao/releases/tag/v1.0.0-rc15">v1.0.0-rc15</a> release:</p><pre>func Generate(option Option) (string, string) {<br>  if option.Width == 0 {<br>    option.Width = 240<br>  }</pre><pre>  if option.Height == 0 {<br>    option.Width = 80<br>  }</pre><pre>  if option.Length == 0 {<br>    option.Length = 6<br>  }</pre><pre>  if option.Lang == &quot;&quot; {<br>    option.Lang = &quot;zh&quot;<br>  }<br>  ....<br>}</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “option.Height” variable should be used instead of the “option.Width”. <a href="https://github.com/YaoApp/yao/blob/8999225a4ca612cb665f81f47bd65b2c89a10cd3/utils/captcha/captcha.go#L48">captcha.go 48</a></p><h3>CDS</h3><p><a href="https://github.com/ovh/cds">CDS</a> is an enterprise-grade platform for automating DevOps processes, continuous integration (CI), and continuous delivery (CD).</p><p>We found this code in the <a href="https://github.com/ovh/cds/releases/tag/v0.54.1">0.54.1</a> release:</p><pre>if conf.GraylogFieldCDSServiceName != &quot;&quot; {<br>  extra[&quot;CDSName&quot;] = conf.GraylogFieldCDSServiceName<br>}<br>if conf.GraylogFieldCDSServiceName != &quot;&quot; {<br>  extra[&quot;CDSService&quot;] = conf.GraylogFieldCDSServiceType<br>}<br>if conf.GraylogFieldCDSVersion != &quot;&quot; {<br>  extra[&quot;CDSVersion&quot;] = conf.GraylogFieldCDSVersion<br>}<br>if conf.GraylogFieldCDSOS != &quot;&quot; {<br>  extra[&quot;CDSOS&quot;] = conf.GraylogFieldCDSOS<br>}<br>if conf.GraylogFieldCDSArch != &quot;&quot; {<br>  extra[&quot;CDSArch&quot;] = conf.GraylogFieldCDSArch<br>}</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “conf.GraylogFieldCDSServiceType” variable should be used instead of the “conf.GraylogFieldCDSServiceName”. <a href="https://github.com/ovh/cds/blob/a58c0035cb4fc99830d5604e4657d4a1d577e2c5/sdk/log/log.go#L160">log.go 107</a></p><h3>BFE</h3><p><a href="https://github.com/bfenetworks/bfe">BFE</a> is a high-performance, application-level load balancer and reverse proxy server.</p><p>We found this code in the <a href="https://github.com/bfenetworks/bfe/releases/tag/v1.8.4">1.8.4</a> release:</p><pre>if cfg.Basic.WasmPluginPath == &quot;&quot; {<br>  ....<br>  cfg.Basic.WasmPluginPath = &quot;mod_wasm&quot;<br>}<br>cfg.Basic.WasmPluginPath = bfe_util.ConfPathProc(....)</pre><pre>if cfg.Basic.DataPath == &quot;&quot; {<br>  ....<br>  cfg.Basic.WasmPluginPath = &quot;mod_wasm/wasm.data&quot;<br>}<br>cfg.Basic.DataPath = bfe_util.ConfPathProc(cfg.Basic.DataPath, <br>                                           confRoot)</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “cfg.Basic.DataPath” variable should be used instead of the “cfg.Basic.WasmPluginPath”. <a href="https://github.com/bfenetworks/bfe/blob/b9fb26f3e7be27b4caf6b1a5540fb2dcb9b5659a/bfe_modules/mod_wasmplugin/conf_mod_wasmplugin.go#L63">conf_mod_wasmplugin.go 63</a></p><h3>Hertz</h3><p><a href="https://github.com/cloudwego/hertz">Hertz</a> is a high-performance HTTP framework written in Go by ByteDance (the creators of TikTok), designed for building high-load microservices. It combined the concepts of popular Go frameworks (Gin, FastHTTP) with ByteDance’s internal requirements.</p><p>We found this code in the <a href="https://github.com/cloudwego/hertz/releases/tag/v0.10.6">0.10.6</a> release:</p><pre>if goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] != &quot;&quot; {<br>  reqName = goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] + <br>            &quot;.&quot; + inputGoType.GoIdent.GoName<br>}<br>if goOptMapAlias[sb.Scope.GetOptions().GetGoPackage()] != &quot;&quot; {<br>  respName = goOptMapAlias[st.Scope.GetOptions().GetGoPackage()] + <br>             &quot;.&quot; + outputGoType.GoIdent.GoName<br>}</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “st” variable should be used instead of the “sb”. <a href="https://github.com/cloudwego/hertz/blob/3415de4ca25f550996dab49d62839ebc6165d6b9/cmd/hz/protobuf/ast.go#L233">ast.go 233</a></p><h3>Nuclei</h3><p><a href="https://github.com/projectdiscovery/nuclei/tree/dev">Nuclei</a> is a modern, high-performance vulnerability scanner that uses simple YAML-based templates. It enables users to create their own vulnerability detection scenarios that simulate real-world conditions, which eliminates false positives.</p><p>We found this code in the <a href="https://github.com/projectdiscovery/nuclei/releases/tag/v3.11.1">3.11.1</a> release:</p><pre>for _, tag := range config.ExcludeTags {<br>  for _, val := range splitCommaTrim(tag) {<br>    if _, ok := filter.block[val]; !ok {<br>      filter.block[val] = struct{}{}<br>    }<br>  }<br>}<br>for _, tag := range config.Severities {<br>  if _, ok := filter.severities[tag]; !ok {<br>    filter.severities[tag] = struct{}{}<br>  }<br>}<br>....<br>for _, tag := range config.ExcludeProtocols {<br>  if _, ok := filter.excludeTypes[tag]; !ok {<br>    filter.excludeTypes[tag] = struct{}{}<br>  }<br>}<br>for _, id := range config.ExcludeIds {<br>  for _, val := range splitCommaTrim(id) {<br>    if _, ok := filter.block[val]; !ok {<br>      filter.excludeIds[val] = struct{}{}<br>    }<br>  }<br>}</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “filter.excludeIds” variable should be used instead of the “filter.block”. <a href="https://github.com/projectdiscovery/nuclei/blob/bb79d8ed71fcb71b49ca0563ecadbb3083857cd9/pkg/templates/tag_filter.go#L434">tag_filter.go 434</a></p><h3>LakeFS</h3><p><a href="https://github.com/treeverse/lakeFS">LakeFS</a> is an open-source data version control system for managing data stored in an object storage system. It enables Git-like operations like branching, committing, merging, and rolling back for large-scale data, stored in systems including Amazon S3, Azure Blob Storage, and Google Cloud Storage, as well as on other S3-compatible object storage platforms.</p><p>We found this code in the <a href="https://github.com/treeverse/lakeFS/releases/tag/v1.86.0">1.86.0</a> release:</p><pre>usersKey := model.GroupUserPath(groupID, &quot;&quot;)</pre><pre>it, err := kv.NewSecondaryIterator(....)</pre><pre>if err != nil {<br>  return err<br>}<br>defer it.Close()<br>for it.Next() {<br>  entry := it.Entry()<br>  user := entry.Value.(*model.UserData)<br>  ....<br>}<br>if err = it.Err(); err != nil {<br>  return err<br>}</pre><pre>policiesKey := model.GroupPolicyPath(groupID, &quot;&quot;)</pre><pre>itr, err := kv.NewSecondaryIterator(....)</pre><pre>if err != nil {<br>  return err<br>}<br>defer it.Close()<br>for itr.Next() {<br>  entry := itr.Entry()<br>  policy := entry.Value.(*model.PolicyData)<br>  ....<br>}<br>if err = itr.Err(); err != nil {<br>  return err<br>}</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “itr” variable should be used instead of the “it”. <a href="https://github.com/treeverse/lakeFS/blob/4bb11638e95637e853d9680abf072b14f09e32fb/contrib/auth/acl/service.go#L476">service.go 476</a></p><h3>Vault</h3><p><a href="https://github.com/hashicorp/vault">Vault</a> is an open-source (BSL) tool for centralized secret management, data encryption, and for the protection of confidential information in cloud and hybrid infrastructures.</p><p>We found this code in the <a href="https://github.com/hashicorp/vault/releases/tag/v2.0.4">2.0.4</a> release:</p><pre>for name, crl := range h.CRLs {<br>  ....<br>  ratio := time.Duration(<br>    (int64(total) * int64(h.CRLExpiryPercentage)) / int64(100)<br>  )<br>  if used &gt;= ratio {<br>    expWhen := crl.ThisUpdate.Add(ratio)<br>    ret.Status = ResultCritical<br>    ret.Message = fmt.Sprintf(<br>      ...., <br>      crl.NextUpdate.Format(&quot;2006-01-02&quot;), <br>      h.CRLExpiryPercentage,   <br>      crl.ThisUpdate.Format(&quot;2006-01-02&quot;), <br>      expWhen.Format(&quot;2006-01-02&quot;)<br>    )<br>    ....<br>  }</pre><pre>  results = append(results, &amp;ret)<br>}</pre><pre>for name, crl := range h.DeltaCRLs {<br>  ....<br>  ratio := time.Duration(<br>    (int64(total) * int64(h.DeltaCRLExpiryPercentage)) / int64(100)<br>  )<br>  if used &gt;= ratio {<br>    expWhen := crl.ThisUpdate.Add(ratio)<br>    ret.Status = ResultCritical<br>    ret.Message = fmt.Sprintf(<br>      ...., <br>      crl.NextUpdate.Format(&quot;2006-01-02&quot;),<br>      h.CRLExpiryPercentage, <br>      crl.ThisUpdate.Format(&quot;2006-01-02&quot;), <br>      expWhen.Format(&quot;2006-01-02&quot;)<br>    )<br>    ....<br>  }</pre><pre>  results = append(results, &amp;ret)<br>}</pre><p>The analyzer’s hint:</p><p>V8033 Two similar code fragments were found. The second fragment may contain a typo: the “h.DeltaCRLExpiryPercentage” variable should be used instead of the “h.CRLExpiryPercentage”. <a href="https://github.com/hashicorp/vault/blob/2beda6097e3409b0ffc56c4bd23965daa661a112/command/healthcheck/pki_crl_validity_period.go#L195">pki_crl_validity_period.go 195</a></p><h3>Conclusion</h3><p>Did you spot all the errors without looking at the hints? If so, that’s great! If not, don’t worry — that’s where a static code analyzer comes into play!</p><p>By the way, you can download it from our <a href="https://pvs-studio.com/en/pvs-studio/try-free/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1419">official website</a> and try it for free on your own projects.</p><p>In addition to the Go analyzer, PVS-Studio 8.00 brought several other exciting features, including an analyzer for JavaScript and TypeScript. You can find more details about them in our <a href="https://pvs-studio.com/en/blog/posts/1406/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1419">article</a>.</p><p>Happy coding!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=8e43a804304a" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/when-copy-paste-goes-wrong-bugs-in-structurally-isomorphic-go-code-8e43a804304a">When copy-paste goes wrong: Bugs in structurally isomorphic Go code</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How we made JavaScript analyzer understand control flow]]></title>
            <link>https://medium.com/pvs-studio/how-we-made-javascript-analyzer-understand-control-flow-7d59c1f5fcc9?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/7d59c1f5fcc9</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[software-development]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Fri, 09 Oct 2026 12:34:21 GMT</pubDate>
            <atom:updated>2026-10-09T12:34:21.552Z</atom:updated>
            <content:encoded><![CDATA[<p>We recently added JavaScript/TypeScript support to PVS-Studio analyzer. From its first release, the analyzer can detect control-flow bugs in code. In this article, we’ll learn about the control flow graph (CFG), how we’ve implemented it, and why it’s useful.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*Vj6aNNxdBwTuUVfp.png" /></figure><h3>A Control Flow Graph?</h3><p>I’ve touched on this topic before, in my <a href="https://pvs-studio.com/en/blog/posts/java/1198/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">series</a> on taint analysis in Java, but let’s quickly recap it here. A static analyzer starts by converting source code into an abstract syntax tree (AST). It almost completely mirrors the structure of the code as you write it in the editor. That makes it a poor fit for anything involving control flow analysis, such as finding unreachable code. It’s possible to write a check for unreachable code on the AST, but:</p><ul><li>this would be unreliable in corner cases;</li><li>this wouldn’t scale well to similar rules;</li><li>this would be hard to maintain. Every bug fix or new language version would mean revisiting all of these diagnostic rules.</li></ul><p>The Control Flow Graph (CFG) exists to address these issues; it maps out all possible execution paths in a program.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*0EKJtXOr3KUl0JvT.png" /></figure><p>The CFG consists of three components:</p><ul><li>entry and exit nodes;</li><li>Basic blocks nodes, each containing linear instructions;</li><li>Each basic block ends with a terminator (or with nothing). A terminator is a special instruction that determines specific semantics for the execution flow: branching, break/continue, and so on.</li><li>Edges between nodes that represent possible control flow transitions.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*DX2iHNrVRI8zeWD9.png" /></figure><h3>How did we do it?</h3><h3>Common CFG</h3><p>In our <a href="https://pvs-studio.com/en/blog/posts/js/1363/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">article</a> on developing the JavaScript/TypeScript analyzer, we <a href="https://pvs-studio.com/en/blog/posts/js/1363/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418#IDC01AE1A5F1">mentioned</a> that the tool includes not only a syntax tree for each specific language, but also a common one for multilingual analysis. We named it CAT (Common Abstract Tree).</p><p>No other languages have joined JavaScript/TypeScript yet, but we’ve already made the CFG extensible: the core engine doesn’t depend on any specific language. We define a set of multiple semantics and just combine them like building blocks into a CFG for a specific language we need. It’ll save time when it comes to supporting other languages in the future.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*_xAY3jx5fmdKReku.png" /></figure><p>Here are two examples of language-specific semantics for JavaScript/TypeScript:</p><ul><li>The try blocks in which exceptions are untyped, and only one catch is allowed.</li><li>Labels are attached to a specific statement, and you can jump to them only from the break statement nested within that statement. You can also jump to the label using continue if it is inside a loop.</li></ul><h3>Testing approach</h3><p>Checking that a CFG is correct is quite an adventure of its own. The first thing that comes to mind is to serialize the graph in <a href="https://en.wikipedia.org/wiki/Graphviz">Graphviz</a> (or some other way), check it visually, and save it as a reference. Spoiler: that was a trap, and we fell into it. Here’s why it doesn’t work:</p><ul><li>The graph changes constantly during development. You end up manually rewriting the failing tests or burning through AI agents’ tokens.</li><li>It’s very easy to miss a bug in the reference. If people didn’t make mistakes, the static analysis industry wouldn’t exist.</li><li>A neat-looking topology doesn’t guarantee that the graph captures the full semantics of the control flow.</li></ul><p>So we changed the approach and wrote a mini-interpreter for our CAT, driven by the CFG. The idea is simple: if our interpreter, after traversing the graph, produces the same result as the JavaScript interpreter, the graph is constructed correctly. We verify this with a simple assertion API, like this:</p><pre>@Test<br>void branching() {<br>    EvaluationAssert.evaluate(&quot;branching&quot;)<br>                    .withParam(&quot;param&quot;, true)<br>                    .expect(&quot;a&quot;, 2);</pre><pre>    EvaluationAssert.evaluate(&quot;branching&quot;)<br>                    .withParam(&quot;param&quot;, false)<br>                    .expect(&quot;a&quot;, 0);<br>}</pre><p>This test runs the interpreter and compares the execution results with the reference, which you can get by running the TypeScript code beforehand:</p><pre>function branching(param: boolean) {<br>    let a = 1;<br>    if (param) {<br>        a++;<br>    } else {<br>        a--;<br>    }<br>}</pre><p>We didn’t have to implement the entire JavaScript specification — a tiny subset was enough. Cutting corners actually worked in our favor: if you don’t clear variables when they go out of scope, you can inspect the state of the code at different points.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*e2J1xZkc-Y-Fp6qa.png" /></figure><p>Another nice bonus: the minimalist interpreter is a full-fledged proof of concept for graph processing. It can serve both as a reference for using the API and as groundwork for upcoming data-flow analysis.</p><p>The result is a working graph builder for JavaScript/TypeScript that constructs each graph in a single AST traversal. It’s quite fast: for <a href="https://www.google.com/search?client=firefox-b-d&amp;q=react&amp;sei=_ymVao7PAuev5NoPzafroAY">React</a>, all graphs are built in a quarter of a second.</p><h3>What it can catch today</h3><h3>Linear code</h3><p>The most basic typos, the kind the AST could catch too, come first, of course:</p><pre>if ( curveLengths[ i ] &gt;= d ) {<br>    diff = curveLengths[ i ] - d;<br>    curve = this.curves[ i ];<br>    var u = 1 - diff / curve.getLength();<br>    return curve.getPointAt( u );<br>    break;<br>}</pre><p>The PVS-Studio warning: <a href="https://pvs-studio.com/en/docs/warnings/v7039/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">V7039</a> Unreachable code detected. Control flow never reaches this statement. <a href="https://github.com/juice-shop/juice-shop/blob/1618a611b173b4bf114028e6e02549950606e29d/frontend/src/assets/private/three.js#L29417">three.js 29417</a>.</p><p>That’s the graph built by the analyzer:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*timb0cREfTLkeEED.png" /></figure><p>What are merge nodes?</p><p>A merge node is an auxiliary node used during graph construction. It makes the graph easier to work with, because nodes with multiple predecessors are cumbersome to handle. The analysis doesn’t use merge nodes (yet).</p><p>In the example above, the merge has only one incoming edge instead of two, as in the previous scheme, because the true branch contains a return, which requires an edge to be created straight to the exit node.</p><p>The graph shows clearly no path lead to break, so the analyzer marks it as unreachable.</p><p>It’s unlikely that this error has any effect, and the unreachable break is probably just redundant. But here&#39;s the interesting part: this file is the <a href="https://github.com/mrdoob/three.js/">Three.js</a> library, copied into <a href="https://github.com/juice-shop/juice-shop/">Juice Shop</a>, and we couldn&#39;t find the same error in the current Three.js repository.</p><p>By the way, here’s a classic bug involving automatic semicolon insertion (ASI):</p><pre>function foo() {<br>    return // asi happens here<br>        this.bar<br>}</pre><p>The analyzer catches it the same way. The inserted ; creates a terminator in the block containing the return and moves this.bar to the next block:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*ws8z8CzvOVR2J9D0.png" /></figure><h3>Conditions</h3><p><a href="https://github.com/phaserjs/phaser">Phaser</a> gives us a good example of “defensive programming”:</p><pre>if (!childA.parentContainer &amp;&amp; !childB.parentContainer)<br>{<br>    return this.displayList.getIndex(childB)<br>      - this.displayList.getIndex(childA);<br>}<br>else if (childA.parentContainer === childB.parentContainer) {<br>// more branches ending with return statements here<br>} else<br>{<br>    var listA = childA.getIndexList();<br>    var listB = childB.getIndexList();<br>    var len = Math.min(listA.length, listB.length);</pre><pre>    for (var i = 0; i &lt; len; i++)<br>    {<br>        var indexA = listA[i];<br>        var indexB = listB[i];</pre><pre>        if (indexA === indexB)<br>        {<br>            continue;<br>        }<br>        else<br>        {<br>            return indexB - indexA;<br>        }<br>    }<br>    return listB.length - listA.length;<br>}<br>//  Technically this shouldn&#39;t happen, but ...<br>// eslint-disable-next-line no-unreachable<br>return 0;</pre><p>The PVS-Studio warning: <a href="https://pvs-studio.com/en/docs/warnings/v7039/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">V7039</a> Unreachable code detected. Control flow never reaches this statement. <a href="https://github.com/phaserjs/phaser/blob/02d8931b626d9764c133cbb3fbf99966c03c757c/src/input/InputPlugin.js#L2981">InputPlugin.js 2981</a></p><p>There were more else-if branches, but I removed them to keep the example short and to the graph small:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*47ukYlodw8Fx88su.png" /></figure><p>If you peruse the comment and look at the code, the programmers’ concern starts making sense: the logic above is complex, merging more than 5 branches, each of which terminates the execution flow. Devs decided to play it safe, not even trusting ESLint. Following its trail, the analyzer saw from the graph’s topology that there is simply no path to return 0.</p><h3>Loops</h3><p>As a bonus, we also get the ability to identify loops that run infinitely or, conversely, only once. I found such a case in the already mentioned Three.js:</p><pre>loop:<br>for (var pos = 0; pos &lt; limit; pos++) {<br>    for (; pos &lt; limit; pos++) {                 // &lt;=<br>        for (var k = 0; k &lt; needleLength; k++) {<br>            if (haystack[pos + k] !== needle[k]) {<br>                continue loop;<br>            }<br>        }</pre><pre>        return pos;<br>    }<br>}</pre><p>The PVS-Studio warning: <a href="https://pvs-studio.com/en/docs/warnings/v7039/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">V7039</a> Unreachable code detected. Control flow never reaches this statement. <a href="https://github.com/mrdoob/three.js/blob/aa625d502d4920a84975c80a6438c108678fb4ac/examples/jsm/libs/opentype.module.js#L6109">opentype.module.js 6109</a>.</p><p>Here is the control flow graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*6-wQNQ-MixeamfF_.png" /></figure><p>The diagnostic rule warns that the increment of the pos variable in the loop is unreachable. Looking at the code, you can easily see that all the branches indeed terminate the second for loop, so it runs only once. The graph confirms this—there is no path to the increment.</p><p>This code originally came from <a href="https://github.com/opentypejs/opentype.js">OpenType</a>, but the copied dependency was removed at the time of the writing.</p><h3>Exception handling</h3><p>Nested try blocks and execution flow interruptions from finally are rare andoften considered a code smell, so I couldn&#39;t find any instances in open-source code right away.</p><p>Still, this was one of the trickiest cases to handle due to the complexity of the try-catch-finally semantics. The analyzer has to handle several things:</p><ul><li>Check if the try block contains a catch, a finally, or both;</li><li>Track explicit throw statements and also handle where implicit exceptions lead.</li><li>Override the control flow interruption within the finally block when the control flow interruption occurs in try or catch.</li><li>Propagate the control flow interruption that occurs in finally up through the try block.</li><li>Cover many other edge cases and their combinations.</li></ul><p>Since I still want to show this case, here’s a synthetic example:</p><pre>function tryCatchFinallyCase() {<br>    try {<br>        try {<br>            throw new Error(&quot;Whoops&quot;);<br>        } finally {<br>            console.log(&quot;inner&quot;)<br>        }<br>        return true // V7039<br>    } finally {<br>        console.log(&quot;outer&quot;)<br>    }<br>    return false; // V7039<br>}</pre><p>For this function, the analyzer will construct the following graph:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3qoWHJ7JLqiQzzTl.png" /></figure><p>The diagram clearly shows that the edges themselves carry information about which operation they “remembered” before exiting the function. The analyzer also identifies the unreachable return nodes.</p><h3>What’s next?</h3><p>We’ve walked through the errors that the analyzer can now detect thanks to control flow graph support. It has already proven its capabilities, making it fairly straightforward to implement diagnostic rules <a href="https://pvs-studio.com/en/docs/warnings/v7039/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">V7039</a> (unreachable code) and <a href="https://pvs-studio.com/en/docs/warnings/v7040/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1418">V7040</a> (infinite recursion) by the time PVS-Studio 8.00 was released. Beyond adding new diagnostic rules, other opportunities are opening up:</p><ul><li>Extend the CFG construction by applying it to short-circuit expressions.</li><li>Find dead code as well as unreachable code, using conditional constant propagation. Once we have laid the groundwork for the data-flow analysis engine, we can implement <a href="https://pvs-studio.com/en/blog/terms/6496/">taint analysis</a> (tracking tainted data throughout the program).</li><li>Extend the same engine to other kinds of <a href="https://pvs-studio.com/en/blog/terms/7004/">data-flow analysis</a>, such as detecting null dereference, division by zero, and so on.</li></ul><p>In short, CAT grows the analyzer wider, and the CFG lets us grow it deeper. The ground for developing the new analyzer is now even more productive.</p><h3>Wrap-up</h3><p>That’s the end of the brief overview of the analyzer’s technology. I hope you enjoyed seeing how static analysis works and what kinds of errors control flow analysis can find. If you’ve worked with similar technologies, share your experience in the comments. I’d love to read your stories. The promised article on the inner workings of CAT is still on the way, along with more articles on code quality. Follow us to stay tuned:</p><ul><li>PVS-Studio in <a href="https://x.com/pvs_studio">X</a>;</li><li><a href="https://pvs-studio.com/en/subscribe/">monthly newsletter</a>;</li><li>my personal <a href="https://twitter.com/kvolokhovskii">blog</a>.</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=7d59c1f5fcc9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/how-we-made-javascript-analyzer-understand-control-flow-7d59c1f5fcc9">How we made JavaScript analyzer understand control flow</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[I found some bugs in Create mod and checked them in Minecraft]]></title>
            <link>https://medium.com/pvs-studio/i-found-some-bugs-in-create-mod-and-checked-them-in-minecraft-76c47b0c3129?source=rss----9aaa9654e64---4</link>
            <guid isPermaLink="false">https://medium.com/p/76c47b0c3129</guid>
            <category><![CDATA[minecraft]]></category>
            <category><![CDATA[java]]></category>
            <category><![CDATA[modding]]></category>
            <category><![CDATA[gamedev]]></category>
            <category><![CDATA[programming]]></category>
            <dc:creator><![CDATA[Unicorn Developer]]></dc:creator>
            <pubDate>Tue, 29 Sep 2026 13:33:24 GMT</pubDate>
            <atom:updated>2026-09-29T13:33:24.831Z</atom:updated>
            <content:encoded><![CDATA[<p>This article covers bugs in Create, one of the most popular Minecraft mods. Let’s see how they impact the game and then submit pull requests with the necessary fixes.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3x6JbEdFEJcdLS51.png" /></figure><h3>Introduction</h3><p><a href="https://modrinth.com/mod/create">Create</a> is a Minecraft mod that adds shafts, gears, and other mechanical components. You can combine them to build machines powered by rotational force, from a simple windmill to a fully automated <a href="https://www.youtube.com/watch?v=rR8W-f9YhYA">cake factory</a>.</p><p>The mod is hugely popular: it has more than 206 million <a href="https://www.curseforge.com/minecraft/mc-mods/create">downloads on CurseForge</a> alone. That doesn’t even include <a href="https://modrinth.com/mod/create">Modrinth</a>, and the mod keeps getting updates for new versions of the game. I’m pretty sure almost everyone who opened the article has played this mod.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*28CxryTYjnyjwXt0.png" /></figure><h3>Let’s check the project</h3><p>Some important info:</p><ul><li>To check the project, I used PVS-Studio static analyzer. The article author is one of its developers.</li><li>You’ll see code examples as you read. Most of them have been shortened to avoid overwhelming the reader. I’ve also put an ellipsis to mark the shortened code: “….”.</li><li>At the time of the check, the latest revision was the <a href="https://github.com/Creators-of-Create/Create/tree/87b3c6a65fd00c023a07b37b0353144bc7e6a5bf">87b3c6a</a> commit. I checked it using the static analyzer.</li><li>All analyzed source files and any conclusions based on other source files, include a permanent link that you can use to find them.</li><li>The article includes only the bugs that the author found interesting (yes, it’s a matter of personal taste). If you want to see the rest, you can always download the analyzer and check the project yourself.</li></ul><h3>Ctrl + V doesn’t work</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/content/schematics/client/SchematicEditScreen.java#L130">SchematicEditScreen.java(130)</a> file</p><p>Let’s take a look at the menu for pasting a schematic. What’s a schematic? A schematic is a part of the game world saved as a separate file. This lets you move your house from one world to another, for example.</p><pre>public boolean keyPressed(int code, ....) {<br>  if (isPaste(code)) {<br>    String coords = minecraft.keyboardHandler.getClipboard();<br>    if (coords != null &amp;&amp; !coords.isEmpty()) {<br>      coords.replaceAll(&quot; &quot;, &quot;&quot;);             // &lt;=<br>      String[] split = coords.split(&quot;,&quot;);<br>      if (split.length == 3) {<br>        boolean valid = true;<br>        for (String s : split) {<br>          try {<br>            Integer.parseInt(s);<br>          } catch (NumberFormatException e) {<br>            valid = false;<br>          }<br>        }<br>        if (valid) {<br>          xInput.setValue(split[0]);<br>          yInput.setValue(split[1]);<br>          zInput.setValue(split[2]);<br>          return true;<br>        }<br>      }<br>    }<br>  }<br>  ....<br>}</pre><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6010/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6010</a> The return value of function ‘replaceAll’ is required to be utilized. SchematicEditScreen.java 130</p><p>The idea is simple: the player copies the coordinates, presses Ctrl + V in a dedicated window, and the fields fill in automatically.</p><p>But the replaceAll call marked // &lt;= serves no purpose. Strings in Java are immutable, so the method doesn&#39;t modify the original string; it returns a new one instead. The new string is immediately discarded, leaving the spaces instead.</p><p>The code then splits the string by commas, leaving a space at the beginning of the second and third elements, and Integer.parseInt(&quot; 67&quot;) throws a <a href="https://docs.oracle.com/javase/8/docs/api/java/lang/NumberFormatException.html">NumberFormatException</a>. As a result, the fields remain empty.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*A4HHLXyd3wom7HPI.png" /></figure><p>After opening the menu, I saw the expected behavior:</p><ul><li>-156, 67, 204 doesn&#39;t work;</li><li>-156,67,204 works.</li></ul><p>The fix fits on a single line. We just add an assignment before the replaceAll operation:</p><pre>coords = coords.replaceAll(&quot; &quot;, &quot;&quot;);</pre><h3>Unnecessary maximum</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/content/trains/schedule/ScheduleScreen.java#L557">ScheduleScreen.java(557)</a> file</p><pre>for (List&lt;ScheduleWaitCondition&gt; list : entry.conditions) {<br>  int maxWidth = getConditionColumnWidth(list);<br>  for (int i = 0; i &lt; list.size(); i++) {<br>    ScheduleWaitCondition scheduleWaitCondition = list.get(i);<br>    Math.max(maxWidth, renderInput(....));   // &lt;=<br>    scheduleWaitCondition.renderSpecialIcon(....);<br>  }<br>  AllGuiTextures.SCHEDULE_CONDITION_APPEND.render(<br>    graphics, <br>    xOffset + (maxWidth - 10) / 2,<br>    29 + list.size() * 18<br>  );<br>  xOffset += maxWidth + 10;<br>}</pre><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6010/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6010</a> The return value of function ‘max’ is required to be utilized. ScheduleScreen.java 557</p><p>The analyzer warning clearly explains the error: the result of the Math.max(....) call isn&#39;t written anywhere.</p><p>I looked through the commit history, expecting to find some traces of refactoring. I thought the developers might have moved the width calculation to a separate method and removed the assignment to maxWidth but left the Math#max call untouched. There was nothing like that, though. The file first appeared on February 1, 2022, in a <a href="https://github.com/Creators-of-Create/Create/commit/576d00d3a0e502d418488ef463ad7eb4237560ab#diff-59b447844d14ec067c0449bccbecf6179277ee5603f60cc4cb682911967f0bc8">single 941-line commit</a>, and the Math#max call without an assignment had been there all along. The original initialization of maxWidth with a call to getConditionColumnWidth() was there too.</p><p>What would change if the devs added maxWidth = Math.max(....)? Nothing. The column width where the text appears (see the image below) is calculated in advance with some extra room: getConditionColumnWidth() goes through the entire column and takes the maximum width, so no line can extend beyond it. I checked this using the debugger: in every case I was able to test, it returned the same width. So, the code is searching for the maximum of two identical numbers.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*IjaWTXVXlqPZutyo.png" /></figure><p>The line didn’t break over time, nor was it affected by any refactoring. From day one, it had done nothing — for four years and through several updates to newer Minecraft versions.</p><p>I’d suggest removing the unnecessary wrapper and leaving renderInput on its own.</p><h3>Ignore error handling</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/content/kinetics/crafter/MechanicalCrafterBlockEntity.java#L535">MechanicalCrafterBlockEntity.java(535)</a> file</p><p>First of all, let me show you what the mechanical crafter looks like:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cYXrxFsOSUkuoK4I.png" /></figure><p>Image explanation:</p><p>In vanilla Minecraft, players craft items at a crafting table by manually placing ingredients in specific slots. A mechanical crafter does the same thing automatically. You build a grid from several blocks, with each block corresponding to one slot in the recipe, and place the required ingredient in each one. The screenshot shows the recipe for an iron pickaxe: three ingots in the top row and two sticks down the middle.</p><p>The arrows on the front indicate the route. The crafters pass items from one to another along the chain until everything reaches the final block. That’s where the recipe gets assembled, and the finished item moves on to a conveyor belt or into a chest.</p><p>The whole system runs on rotational power, so it needs a drive connected to the side. In the screenshot, a creative motor powers the grid.</p><p>Let’s take a look at a method in its logic:</p><pre>protected void continueIfAllPrecedingFinished() {<br>  List&lt;MechanicalCrafterBlockEntity&gt; preceding = <br>    RecipeGridHandler.getPrecedingCrafters(this);</pre><pre>  if (preceding == null) { // &lt;=<br>    ejectWholeGrid();<br>    return;<br>  }</pre><pre>  for (MechanicalCrafterBlockEntity blockEntity : preceding)<br>    if (blockEntity.phase != Phase.WAITING)<br>      return;<br>  <br>  phase = Phase.ASSEMBLING;<br>  countDown = 1;<br>}</pre><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6007/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6007</a> Expression ‘preceding == null’ is always false. New returns not-null reference. MechanicalCrafterBlockEntity.java 535</p><p>The getPrecedingCrafters method returns the crafters that come earlier in the chain. In other words, the ones that feed items into the current crafter. It can never return null. A list is created at the start, and both method exits return it. Even if the block isn&#39;t a crafter, the method returns an empty list:</p><pre>public static List&lt;MechanicalCrafterBlockEntity&gt; getPrecedingCrafters(<br>  MechanicalCrafterBlockEntity crafter<br>) {<br>  BlockPos pos = crafter.getBlockPos();<br>  Level world = crafter.getLevel();<br>  List&lt;MechanicalCrafterBlockEntity&gt; crafters = new ArrayList&lt;&gt;();<br>  BlockState blockState = crafter.getBlockState();</pre><pre>  if (!isCrafter(blockState))<br>    return crafters;</pre><pre>  ....<br>  return crafters;<br>}</pre><p>This means the branch with ejectWholeGrid() is unreachable. The preceding list can never be null, but it can be empty, and often is. What if we replace the check with isEmpty() and test it in the game? Let&#39;s see what happens.</p><p>Before the fix:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*MFT3B7jOECldDcH8.gif" /></figure><p>After the fix with isEmpty:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*L0Q8LIOqAhhSt8li.gif" /></figure><p>The video shows that, before the fix, items entering the loop were passed from one crafter to another indefinitely. That could potentially cause trouble and break someone’s factory :)</p><p>However, after the check was replaced with isEmpty(), the loop didn&#39;t even have a chance to form. The method determined that there was nowhere else to expect items from, so it ejected them.</p><p>However, as soon as I added two more crafters to the chain, the fix stopped working:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/800/0*0eJgVBsvMj2inRFu.gif" /></figure><p>Two options are possible here. Either the mechanical crafter needs to detect loops differently, then simply replacing the check with isEmpty() isn&#39;t enough. Or this looping behavior isn&#39;t a bug at all. In that case, we can just remove the null check.</p><h3>The logging crash</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/content/trains/entity/TrainRelocationPacket.java#L52">TrainRelocationPacket.java(52)</a> file</p><p>Here’s the client-server train movement package handler:</p><pre>public void handle(ServerPlayer sender) {<br>  Train train = Create.RAILWAYS.trains.get(trainId);<br>  Entity entity = sender.level().getEntity(entityId);</pre><pre>  String messagePrefix = sender.getName()<br>      .getString() + &quot; could not relocate Train &quot;;</pre><pre>  if (<br>    train == null || // &lt;=<br>    !(entity instanceof CarriageContraptionEntity cce)<br>  ) {<br>    Create.LOGGER.warn(messagePrefix + train.id.toString()<br>        .substring(0, 5) + &quot;: not present on server&quot;);<br>    return;<br>  }</pre><pre>  if (!train.id.equals(cce.trainId))<br>    return;</pre><pre>  ....<br>}</pre><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6008/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6008</a> Potential null dereference of ‘train’. TrainRelocationPacket.java 52</p><p>The condition triggers in two cases: either the train can’t be found, or the entity is of the wrong class. The second case is fine: it logs a proper warning. However, in the first case, the code accesses train.id when train is null.</p><p>The funny part is that the message we’re trying to log ends with not present on server. The branch was created for a missing train, but it fails because of it.</p><p>The packet comes from the client, meaning it chooses the train ID. A modified client build can send requests to transfer nonexistent trains and make the server fill its log with stack traces.</p><p>The server won’t crash: Create registers its packets so they execute safely, and even if an exception occurs, it gets logged, while the packet simply goes unhandled.</p><p>We can fix this by including the identifier in the package:</p><pre>messagePrefix + trainId.toString().substring(0, 5)</pre><h3>The two-state theory</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/content/kinetics/mixer/MechanicalMixerBlockEntity.java#L238">MechanicalMixerBlockEntity.java(238)</a> file</p><pre>protected List&lt;Recipe&lt;?&gt;&gt; getMatchingRecipes() {<br>  List&lt;Recipe&lt;?&gt;&gt; matchingRecipes = super.getMatchingRecipes();  <br>  if (!AllConfigs.server().recipes.allowBrewingInMixer.get())<br>    return matchingRecipes;</pre><pre>  Optional&lt;BasinBlockEntity&gt; basin = getBasin();<br>  if (!basin.isPresent())<br>    return matchingRecipes;</pre><pre>  BasinBlockEntity basinBlockEntity = basin.get();<br>  if (basin.isEmpty()) // &lt;= <br>    return matchingRecipes;</pre><pre>  IItemHandler availableItems = level.getCapability(<br>  Capabilities.ItemHandler.BLOCK, <br>  basinBlockEntity.getBlockPos(), null<br>  );</pre><pre>  if (availableItems == null)<br>    return matchingRecipes;</pre><pre>  for (int i = 0; i &lt; availableItems.getSlots(); i++) {<br>    ....<br>  }<br>  <br>  return matchingRecipes;<br>}</pre><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6007/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6007</a> Expression ‘basin.isEmpty()’ is always true. MechanicalMixerBlockEntity.java 238</p><p>Here, basin.isEmpty() can never return true, so return in this branch is unreachable. Most likely, the developers meant to check basinBlockEntity rather than Optional.</p><p>This doesn’t affect gameplay in any way. Further down, the code has other checks that work with the same block, and if the basin is empty, either the loop doesn’t run or availableItems is null.</p><p>But a dead if is more dangerous than it looks. It&#39;s a safeguard that doesn&#39;t really protect against anything. Rewriting the checks below leaves a hole that no one will think to look for because the check is kind of there.</p><h3>Method contracts must be followed</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/content/contraptions/Contraption.java#L230">Contraption.java(230)</a> file</p><pre>public static Contraption fromNBT(Level world, CompoundTag nbt, <br>                  boolean spawnData) {<br>  String type = nbt.getString(&quot;Type&quot;);<br>  Contraption contraption = ContraptionType.fromType(type);<br>  contraption.readNBT(world, nbt, spawnData);<br>  ....<br>}</pre><p>Can you see the error? What if we take a look inside ContraptionType#fromType?</p><pre>/**<br> * Lookup the ContraptionType with the given ID, <br> * and create a new Contraption from it if present.<br> * If it doesn&#39;t exist, returns null.<br> */<br>@Nullable<br>public static Contraption fromType(String typeId) {<br>  ContraptionType legacy = AllContraptionTypes.BY_LEGACY_NAME<br>                        .get(typeId);<br>  if (legacy != null) {<br>    return legacy.factory.get();<br>  }<br>  ResourceLocation id = ResourceLocation.tryParse(typeId);<br>  ContraptionType type = CreateBuiltInRegistries.CONTRAPTION_TYPE.get(id);<br>  return type == null ? null : type.factory.get();<br>}</pre><p>Now it’s clear. The method explicitly warns about null twice: with an annotation and a comment. However, the caller code doesn&#39;t check anything.</p><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6008/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6008</a> Potential null dereference of ‘contraption’. Contraption.java 230</p><p>It’s easier to break than it seems. The very first line calls nbt.getString(&quot;Type&quot;), and if the key doesn&#39;t exist, an empty string is returned. This type isn&#39;t in the registry either, so the code returns null again.</p><p>We can verify this pretty easily by giving ourselves a minecart with a nonexistent contraption type:</p><pre>/give @p<br>create:minecart_contraption[create:minecart_contraption_data=<br>{Type:&quot;create:does_not_exist&quot;,InitialOrientation:&quot;north&quot;}]</pre><p>What is a contraption?</p><p>A contraption is a structure that the mod detaches from the world and turns into a single movable object: a platform on a minecart, a piston carrying a load, or a rotating bridge. The blocks inside stop being part of the world and are stored as data belonging to a single entity instead.</p><p>The command won’t break anything, and the item will go into the inventory without a problem. But once we put the minecart on the rails, MinecartContraptionItem will try to build a contraption from this data, and that&#39;s where we&#39;ll get a <a href="https://gist.github.com/TheLivan/ad251e6a4357230f1181eb59ae06d7c3">stack trace with </a><a href="https://gist.github.com/TheLivan/ad251e6a4357230f1181eb59ae06d7c3">NullPointerException</a>. Minecraft will replace the player&#39;s contraption with a vanilla minecart.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*8SSKgcy7sWFQMbnS.png" /></figure><p>We can easily add the check:</p><pre>Contraption contraption = ContraptionType.fromType(type);<br>if (contraption == null) {<br>  Create.LOGGER.warn(&quot;Unknown contraption type: {}&quot;, type);<br>  return null;<br>}</pre><p>But then null will need to be handled in both code snippets that call it. That raises the question of what to do with a minecart whose contents the mod can no longer understand. Should we leave it as an empty minecart, or simply not load the entity at all? I think it&#39;s worth asking the mod developers. They clearly know better than anyone else what to do here.</p><h3>Interruption</h3><p>The <a href="https://github.com/Creators-of-Create/Create/blob/0924e93639ad5f61cfc39a221d909e16f2893df1/src/main/java/com/simibubi/create/infrastructure/command/ServerLagger.java#L14">ServerLagger.java(14)</a> file</p><pre>public void tick() {<br>  if (!isLagging || tickTime &lt;= 0)<br>    return;</pre><pre>  try {<br>    Thread.sleep(tickTime);<br>  } catch (InterruptedException e) {<br>    e.printStackTrace();<br>  }<br>}</pre><p>The PVS-Studio warning:</p><p><a href="https://pvs-studio.com/en/docs/warnings/v6103/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">V6103</a> Ignored InterruptedException could lead to delayed thread shutdown. ServerLagger.java 14</p><p>ServerLagger is a helper class for the killtps start &lt;tickTime&gt; debug command that puts the server to sleep for the specified number of milliseconds on every tick, allowing to see how the mod behaves when the TPS drops. It&#39;s available only in debug builds and only to operators (that&#39;s what server administrators are called), so there&#39;s no need to worry. However, the code is still incorrect, and it&#39;s worth explaining why.</p><p>When the thread is interrupted, sleep immediately throws an InterruptedException and also clears the interrupt flag. In other words, after catch, it seems like nothing&#39;s left: no flag, no exception, just a line in System.err. The method returns control without issue, and the server continues running as if it had never been called.</p><p>An <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/interrupt.html">interrupt</a> is a request for the entire thread to stop, not just the part where you catch it. By interrupting the thread and doing nothing, you take that request and throw it away. The <a href="https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/lang/InterruptedException.html">Javadoc</a> explicitly points this out. One line is enough to pass the interrupt:</p><pre>} catch (InterruptedException e) {<br>  Thread.currentThread().interrupt();<br>}</pre><h3>Conclusion</h3><p>That’s all for today. I collected all the bugs both in this article and in pull requests for the project’s developers (<a href="https://github.com/Creators-of-Create/Create/pull/10696">click</a>, <a href="https://github.com/Creators-of-Create/Create/pull/10721">click</a>).</p><p>Want to try static analysis too? You can always integrate it into your own project with PVS-Studio. You can download a trial version <a href="https://pvs-studio.com/en/pvs-studio/try-free/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">here</a>. The license is <a href="https://pvs-studio.com/en/order/open-source-license/?utm_source=website&amp;utm_medium=medium&amp;utm_campaign=article&amp;utm_content=1417">free for open-source projects</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=76c47b0c3129" width="1" height="1" alt=""><hr><p><a href="https://medium.com/pvs-studio/i-found-some-bugs-in-create-mod-and-checked-them-in-minecraft-76c47b0c3129">I found some bugs in Create mod and checked them in Minecraft</a> was originally published in <a href="https://medium.com/pvs-studio">PVS-Studio</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>